JavaScript for Backend Developers — Part 7: Ship It — AI, Observability & Production Mastery + 3 Complete Capstone Projects | FreeLearning365

JavaScript for Backend Developers — Part 7: Ship It — AI, Observability & Production Mastery + 3 Complete Capstone Projects | FreeLearning365
🚀 JavaScript for Backend Developers
🗺️ The Complete 7-Part Journey
Navigate anywhere in the series
Learning Path Part 1 of 7
🏆 Part 7 → AI + Observability + Production + 3 Capstone Projects
JavaScript for Backend Developers — Part 7: Ship It — AI, Observability & Production Mastery + 3 Complete Capstone Projects | FreeLearning365
Part 7 of 7 · The Final Chapter · The Grand Finale

Ship It — From Localhost to Production.
AI, Observability & Three Capstones.

Six parts. Hundreds of concepts. One final chapter where everything comes together. Learn how senior engineers work with AI, observe their systems, deploy safely, and ship with confidence — then build three complete production-grade projects from scratch. This is where you stop learning JavaScript and start operating it.

📖 ~90 min deep read 🏗️ 3 complete capstone projects 🎮 12 interactive demos ⚡ 150+ code examples 🏆 Series finale

00 · Introduction — The Last Mile

Six months after the incident. Nadia sat in a coffee shop, laptop open, watching her CI pipeline turn green. Three hundred and forty-seven tests. 92% branch coverage. TypeScript strict mode. Zero ESLint warnings. The deploy was going out on a Friday afternoon — something she'd never have done six months ago.

The new junior engineer sat next to her. "How are you so calm about Friday deploys?" she asked.

Nadia smiled. "Because I finally understand the last mile. Writing code is the easy part. The hard part — the part that separates a script from a product — is everything that happens after the code is written."

This final part is about that last mile. The tools, patterns, and habits that turn code into systems that run reliably in production.

You've come a long way. You understand the event loop, async patterns, the object model, modern syntax, the browser runtime, and how to test and type your code. In this final part, we cover the last 10% that takes 90% of the effort — the production concerns that turn a working program into a reliable product.

What You'll Master in Part 7

🤖

AI-Assisted Development

How to pair with Claude, Copilot, and Cursor to 5× your productivity — without losing control of your code.

🔭

Observability

Structured logging with Pino, metrics with Prometheus, distributed tracing with OpenTelemetry.

📊

Performance Profiling

Finding the real bottlenecks in production with flame graphs, event loop lag, and memory analysis.

🚀

Deployment Strategies

Blue-green, canary, feature flags — the safe ways to ship to millions of users.

🛡️

Security

Helmet, CORS, rate limiting, secrets management, and how to think like an attacker.

🏆

Three Capstone Projects

URL Shortener API. Real-Time Collaborative Tasks. AI Content Moderation. All complete.

🎓 ✨ 🚀

The Series Finale

When you finish this article, you'll have gone from "backend developer who panics about JavaScript" to "backend developer who ships production JavaScript with confidence". That's not a small transformation. It's the whole point of this series.

01 · AI-Assisted Development

Nadia's first day with Claude. She pasted a 200-line service file and typed: "Review this. Find bugs, suggest improvements, and rewrite the parts that are hard to test."

Sixty seconds later, Claude identified four issues — including a subtle one she'd missed: an async call inside a loop that should have been batched. It wasn't a bug yet, but it would have been a p99 latency problem under load.

"This feels like cheating," she told Anwar.

"It's not cheating. It's compounding. You still did the thinking. The AI did the remembering, the pattern-matching, the second-pair-of-eyes. The review is still yours."

The Three AI Tools You Need to Know

🟣
Claude
Best for: architecture, code review, explanation, refactoring
🐙
GitHub Copilot
Best for: inline completion, boilerplate, repetitive code
🎯
Cursor
Best for: multi-file edits, codebase-wide refactoring
🔷
ChatGPT / GPT-4
Best for: research, alternatives, brainstorming

You don't need all four. Most engineers settle on one primary tool and one secondary. The skill that matters isn't which tool you use — it's how you use it.

The Five Prompt Patterns That Change Everything

ai-prompt-patterns.txt Prompts
// 1. THE REVIEW PATTERN
// Instead of "write this code", ask for critique:
"Review this function as if you were a staff engineer at Stripe.
Identify: correctness bugs, race conditions, performance issues,
missing edge cases, and security concerns. Rank by severity."

// 2. THE TEACH PATTERN
// Understand before accepting:
"Explain what this code does, line by line. Where might it fail?
What assumptions does it make about its inputs?"

// 3. THE REFACTOR PATTERN
// Structure improvements, not just fixes:
"Refactor this to separate the I/O from the business logic.
The result should be testable without any mocks.
Show the before and after."

// 4. THE TEST PATTERN
// Comprehensive test coverage:
"Write Vitest tests for this function. Cover: happy path,
empty input, null/undefined, boundary values, each error path,
async rejection, and one regression test for the bug mentioned
in the comment. Use AAA pattern."

// 5. THE SCENARIO PATTERN
// Explore trade-offs before deciding:
"I'm building a rate limiter for a public API. Options:
in-memory Map, Redis, sliding window vs fixed window.
For 100k requests/sec across 5 servers, which fits best?
Show the trade-offs and code for the winner."

The Golden Rule of AI-Assisted Development

🧠

AI is a junior engineer with infinite recall and zero judgment.

It knows every API, every pattern, every Stack Overflow answer ever written. It can generate 500 lines of plausible code in seconds. But it doesn't know your business, your constraints, or your tolerance for complexity.

The senior engineer's job — and now yours — is to guide the AI: give it context, evaluate its output, and decide what to keep. You are still the architect. The AI is the intern who never sleeps and never forgets.

Where AI Excels (and Where It Fails)

✅ AI is amazing at

  • Boilerplate generation — configs, DTOs, repetitive patterns.
  • Test scaffolding — 80% of a test suite in seconds.
  • Explaining unfamiliar code — library internals, legacy code.
  • Refactoring across many files — CJS→ESM, callback→async/await.
  • Reviewing your own code — catching what you overlooked.
  • Translating between patterns — Express → Fastify, Jest → Vitest.
  • Documentation and JSDoc comments.
  • Brainstorming approaches to a new problem.

❌ AI struggles with

  • Business logic that depends on your domain knowledge.
  • Anything requiring current library versions or APIs.
  • Architectural decisions with trade-offs specific to your team.
  • Performance tuning in your specific production environment.
  • Security-critical code (always review AI-generated auth).
  • Complex state machines with non-obvious invariants.
  • Integrating with proprietary internal systems.
  • Understanding "why" behind legacy decisions.

Interactive: The AI Pairing Workflow

Interactive — A Real AI-Assisted Workflow
🤖

The discipline that separates AI-powered engineers from AI-replaced ones: never merge code you don't understand. If AI generates something and you can't explain every line to a colleague, you don't own that code — the AI does. When it breaks at 2 AM (and it will), you're the one who fixes it.

02 · Observability — Logs, Metrics, Traces

Three weeks after shipping the fix. Nadia got an alert: p99 latency on the payments endpoint had jumped from 220ms to 1,400ms. But CPU was fine. Memory was fine. No errors in the logs.

Without observability, she'd be guessing. With it, she opened her tracing dashboard and saw the problem in 30 seconds: a downstream service was returning 200 OK but taking 800ms to respond. Every payment call was waiting on it.

She hadn't needed to add any code — the tracing was already there. That's the point of observability. You don't add it during incidents. You add it so incidents become obvious.

The Three Pillars

Pillar Answers Tools
Logs "What happened?" — discrete events with context Pino, Winston, Bunyan, stdout → Loki / Datadog / CloudWatch
Metrics "How is the system behaving?" — numeric time series prom-client, StatsD → Prometheus → Grafana
Traces "Where is time going?" — request flow across services OpenTelemetry → Jaeger / Tempo / Datadog APM
🏥

The hospital metaphor:

Logs = the patient's chart. Every event, in order, with details.
Metrics = the vital signs monitor. Continuous numbers — heart rate, blood pressure, temperature.
Traces = the CT scan. A 3D picture of what happened inside, across every system.

You don't choose one. You need all three to actually diagnose a problem.

Structured Logging with Pino

console.log('user 42 logged in') is not logging. It's noise. Real logging produces structured events with typed fields, that tools can query, filter, and aggregate.

logger.js — Pino setup JavaScript
import pino from 'pino';

// Development: pretty-printed, colorized
// Production: JSON to stdout, consumed by log aggregator
export const logger = pino({
  level: process.env.LOG_LEVEL ?? 'info',
  formatters: {
    level: (label) => ({ level: label })
  },
  base: {
    service: 'payment-service',
    version: process.env.APP_VERSION,
    env: process.env.NODE_ENV
  },
  timestamp: pino.stdTimeFunctions.isoTime,
  redact: {
    // Never log these fields — they leak secrets and PII
    paths: [
      'req.headers.authorization',
      'req.headers.cookie',
      'req.body.password',
      'req.body.cardNumber',
      '*.password',
      '*.apiKey',
      '*.token'
    ],
    remove: true
  }
});

// ❌ BAD — plain text, no structure, impossible to query
console.log(`User ${userId} logged in from ${ip}`);

// ✅ GOOD — structured, queryable, aggregatable
logger.info({
  userId,
  ip,
  userAgent: req.headers['user-agent'],
  event: 'user.login.success'
}, 'User logged in');

// Now in Grafana you can query:
//   count by (event) | event = "user.login.success" | userId = "42"
// Or alert on: anomaly in login rate for a specific user

Request-Scoped Logging with Request IDs

The single most useful logging pattern: every request gets an ID. Every log line inside that request carries the ID. Now you can reconstruct the entire journey of one request.

request-logger.js — correlation IDs JavaScript
import { randomUUID } from 'node:crypto';
import { AsyncLocalStorage } from 'node:async_hooks';

const requestContext = new AsyncLocalStorage();

// Middleware: create context for each request
export function requestContextMiddleware(req, res, next) {
  const requestId = req.headers['x-request-id'] ?? randomUUID();
  const childLogger = logger.child({ requestId });

  res.setHeader('x-request-id', requestId);

  requestContext.run({ requestId, logger: childLogger }, () => {
    const start = Date.now();
    res.on('finish', () => {
      childLogger.info({
        method: req.method,
        path: req.path,
        status: res.statusCode,
        durationMs: Date.now() - start
      }, 'request completed');
    });
    next();
  });
}

// Helper — get the current request logger anywhere
export function getLogger() {
  return requestContext.getStore()?.logger ?? logger;
}

// Usage in a service (no need to pass logger around)
import { getLogger } from './request-logger.js';

export async function createCharge(data) {
  const log = getLogger();
  log.info({ customerId: data.customerId, amountCents: data.amountCents }, 'creating charge');

  try {
    const charge = await bank.charge(data);
    log.info({ chargeId: charge.id, status: charge.status }, 'charge created');
    return charge;
  } catch (err) {
    log.error({ err, customerId: data.customerId }, 'charge failed');
    throw err;
  }
}
🎯

Why this pattern is worth the setup: one log query — { requestId: "abc-123" } — reconstructs the entire lifecycle of a request across every function it touched. This turns 30-minute debugging sessions into 30-second ones.

Metrics with Prometheus

Logs tell you what happened. Metrics tell you how much and how often. They're cheap to store and powerful to alert on.

metrics.js — Prometheus client setup JavaScript
import client from 'prom-client';

// 1. Enable default metrics (CPU, memory, event loop lag, GC, etc.)
client.collectDefaultMetrics({ prefix: 'node_' });

// 2. Counter — only goes up (total requests, total errors)
export const httpRequestsTotal = new client.Counter({
  name: 'http_requests_total',
  help: 'Total HTTP requests',
  labelNames: ['method', 'route', 'status']
});

// 3. Histogram — distribution (request duration, payload size)
export const httpDuration = new client.Histogram({
  name: 'http_request_duration_seconds',
  help: 'HTTP request duration in seconds',
  labelNames: ['method', 'route', 'status'],
  // Buckets chosen so p50, p95, p99 land in distinct buckets
  buckets: [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
});

// 4. Gauge — goes up and down (active connections, queue depth)
export const activeConnections = new client.Gauge({
  name: 'active_db_connections',
  help: 'Active database connections'
});

// Express middleware — measure every request
export function metricsMiddleware(req, res, next) {
  const end = httpDuration.startTimer({
    method: req.method,
    route: req.route?.path ?? req.path
  });

  res.on('finish', () => {
    const labels = {
      method: req.method,
      route: req.route?.path ?? req.path,
      status: res.statusCode
    };
    httpRequestsTotal.inc(labels);
    end({ ...labels });
  });

  next();
}

// Endpoint for Prometheus to scrape
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', client.register.contentType);
  res.end(await client.register.metrics());
});

Once Prometheus scrapes this endpoint, you can query things like:

prometheus-queries.txt — real queries you'll use PromQL
# Requests per second, by route
rate(http_requests_total[1m])

# p95 latency per route
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))

# Error rate (5xx / total)
sum(rate(http_requests_total{status=~"5.."}[5m]))
  /
sum(rate(http_requests_total[5m]))

# Alert: error rate > 1% for 5 minutes
sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) > 0.01

Tracing with OpenTelemetry

Traces show you the full journey of a request through your system — including across microservices, databases, and external APIs. When a request takes 800ms, a trace tells you exactly where those milliseconds went.

tracing.js — OpenTelemetry setup JavaScript
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';

// Auto-instrumentation: catches Express, pg, redis, http, etc.
const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({
    url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? 'http://localhost:4318/v1/traces'
  }),
  instrumentations: [getNodeAutoInstrumentations()],
  serviceName: 'payment-service'
});

sdk.start();

// Manual spans for custom business logic
import { trace } from '@opentelemetry/api';

const tracer = trace.getTracer('payment-service');

export async function processRefund(orderId, amountCents) {
  return tracer.startActiveSpan('processRefund', async (span) => {
    span.setAttributes({
      'order.id': orderId,
      'refund.amount_cents': amountCents
    });

    try {
      const refund = await bank.refund({ orderId, amountCents });
      span.setStatus({ code: 1 });  // OK
      return refund;
    } catch (err) {
      span.recordException(err);
      span.setStatus({ code: 2, message: err.message });  // ERROR
      throw err;
    } finally {
      span.end();
    }
  });
}

Interactive: The Observability Dashboard

Interactive — Diagnose an Incident with Observability

03 · Production Performance Profiling

Week 4. The tracing dashboard showed the bottleneck wasn't the database. It was the JSON serialization on the response. A payload that should have been 5 KB was 200 KB — because someone had accidentally included a full audit log in every response.

"You would have never found that by reading code," Anwar said. "You find it with profiling. The code looked fine. The actual behavior didn't match."

The Node.js Performance Toolkit

🔥

Flame Graphs

Visualize where CPU time is spent. Node's --prof + speedscope.

⏱️

Event Loop Lag

The single most important Node.js metric. Tracks how backed-up the loop is.

💾

Heap Snapshots

Capture memory state, compare over time, find leaks.

📈

Async Hooks

Trace async operations. Available natively or via async_hooks.

🧪

Clinic.js

Suite of tools for CPU, memory, and I/O analysis. Doctor, Flame, Bubbleprof.

Autocannon / k6

Load testing tools — verify performance under realistic traffic.

Event Loop Lag — The Vital Sign

event-loop-monitor.js JavaScript
import { monitorEventLoopDelay } from 'node:perf_hooks';

// Track event loop lag with 20ms resolution
const histogram = monitorEventLoopDelay({ resolution: 20 });
histogram.enable();

// Log the stats every 30 seconds
setInterval(() => {
  const p50 = histogram.percentile(50) / 1_000_000;
  const p95 = histogram.percentile(95) / 1_000_000;
  const p99 = histogram.percentile(99) / 1_000_000;
  const max = histogram.max / 1_000_000;

  logger.info({ p50, p95, p99, max }, 'event loop lag');

  // 🚨 Alert if p99 lag > 100ms — the loop is struggling
  if (p99 > 100) {
    logger.warn({ p99 }, 'HIGH EVENT LOOP LAG — investigate');
  }

  histogram.reset();
}, 30_000);

// What "lag" means:
// - 0-10ms: perfectly healthy
// - 10-50ms: acceptable, slight delay
// - 50-100ms: concern — user-visible latency
// - >100ms: serious — long blocking operations
// - >1s: emergency — likely synchronous CPU-bound work

The Flame Graph Workflow

Flame graphs show where CPU time went. They're the fastest way to find "what's making my service slow".

profiling-workflow.sh Shell
# 1. Run Node with CPU profiling enabled
node --cpu-prof --cpu-prof-dir=./profiles server.js

# 2. Generate load (in another terminal)
npx autocannon -c 100 -d 30 http://localhost:3000/api/users

# 3. Stop the server (Ctrl+C)
# → A .cpuprofile file appears in ./profiles/

# 4. Open in Speedscope (web-based flame graph viewer)
#    Visit https://speedscope.app and drag the .cpuprofile

# Or use the CLI:
npx speedscope ./profiles/CPU*.cpuprofile

# What you'll see:
# - Wide bars at the top = biggest CPU consumers
# - Look for your own code, not Node internals
# - Common culprits: JSON.parse/stringify, regex, sorting, string concat

Real Profiling Story — The 200 KB Response

the-fix.js JavaScript
// ❌ BEFORE: sends everything, including auditLog and internalMetadata
app.get('/api/orders/:id', async (req, res) => {
  const order = await orderRepo.findById(req.params.id, {
    include: ['items', 'customer', 'auditLog', 'internalMetadata']
  });
  res.json(order);  // 200 KB response for a 5 KB order
});

// ✅ AFTER: explicit serialization, only what the client needs
function toOrderResponse(order) {
  return {
    id: order.id,
    status: order.status,
    total: order.total,
    createdAt: order.createdAt,
    items: order.items.map((i) => ({
      sku: i.sku,
      name: i.name,
      quantity: i.quantity,
      price: i.price
    }))
    // Intentionally NOT including: auditLog, internalMetadata, cost, margin
  };
}

app.get('/api/orders/:id', async (req, res) => {
  const order = await orderRepo.findById(req.params.id, {
    include: ['items']  // Only load what we serialize
  });
  if (!order) return res.status(404).json({ error: 'not found' });

  res.json(toOrderResponse(order));  // Now 5 KB
});

// Result:
// - Response size: 200 KB → 5 KB (40× reduction)
// - CPU on serialization: 40ms → 2ms
// - p99 latency: 1400ms → 240ms
// - All from 15 lines of "boring" code
🎯

The most common production performance wins:

1. Explicit response DTOs — never res.json(entity) directly.
2. Database query optimization — N+1 queries are silent killers.
3. Payload size — often the culprit hiding in plain sight.
4. Caching — the fastest code is code that doesn't run.
5. Streaming large responsesres.write() instead of building big arrays.

04 · Deployment Strategies

Month 4. Nadia's team was now deploying 8 times a day. Zero downtime. Zero 2 AM pages in the previous 30 days. But it hadn't always been this way.

"Do you remember the Friday afternoon deploys?" Anwar asked.

"We don't do those anymore," Nadia said. "We do safe deploys. Canary, feature flags, automated rollback. The whole team can push at any time without fear."

The Deployment Strategies

Strategy How It Works Rollback Time Best For
Rolling Replace instances one-by-one Medium Default — simple, no extra infra
Blue-Green Two identical environments, switch traffic Instant When rollback must be sub-second
Canary Send 1% traffic → 10% → 50% → 100% Instant Risky changes, need real-world validation
Feature Flags Deploy dark, enable per user/segment Instant User-facing features, A/B tests
Shadow Mirror traffic to new version, compare N/A (no user impact) High-risk logic changes

Interactive: Canary Deployment

Interactive — Watch a Canary Rollout
v1.4.2 (stable) 100%
🐤 v1.5.0 (canary) 0%

Feature Flags — Ship Dark, Enable Later

Feature flags decouple deployment from release. The code is deployed to production, but the feature is disabled. You can enable it gradually, for specific users, or roll it back instantly without redeploying.

feature-flags.js — simple implementation JavaScript
// Simple in-memory flags. For production, use LaunchDarkly, Unleash, or PostHog.
const flags = {
  'new-checkout': {
    enabled: true,
    rollout: 0.1,        // 10% of users
    allowlist: ['user-42', 'user-88'],  // always included
    denylist: ['user-99']          // always excluded
  },
  'new-payment-provider': {
    enabled: false,
    rollout: 0
  }
};

function isEnabled(flagName, context) {
  const flag = flags[flagName];
  if (!flag?.enabled) return false;

  if (flag.denylist?.includes(context.userId)) return false;
  if (flag.allowlist?.includes(context.userId)) return true;

  // Deterministic rollout — same user gets same answer every time
  const hash = hashUserId(context.userId, flagName);
  return hash < flag.rollout;
}

function hashUserId(userId, flagName) {
  // Simple deterministic hash → [0, 1)
  const str = `${userId}:${flagName}`;
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    hash = (hash << 5) - hash + str.charCodeAt(i);
    hash |= 0;
  }
  return Math.abs(hash) / 2**31;
}

// Usage in a route
app.post('/checkout', async (req, res) => {
  if (isEnabled('new-checkout', { userId: req.user.id })) {
    return newCheckoutController.handle(req, res);
  }
  return legacyCheckoutController.handle(req, res);
});
⚠️

Feature flag hygiene: flags accumulate. Six months after a feature ships, you'll have 40 flags and nobody remembers which ones still matter. Set a policy: every flag has an owner, a creation date, and a sunset date. Delete flags after the feature is fully rolled out and stable.

Health Checks and Graceful Shutdown

Kubernetes and load balancers need to know two things about your service: is it ready to receive traffic, and is it still alive? Separate endpoints for each.

health.js JavaScript
let isReady = false;
let isShuttingDown = false;

// Liveness — is the process healthy? (If not, kill and restart.)
app.get('/health/live', (req, res) => {
  res.status(200).json({ status: 'ok' });
});

// Readiness — can this instance serve traffic? (If not, remove from LB.)
app.get('/health/ready', async (req, res) => {
  if (!isReady || isShuttingDown) {
    return res.status(503).json({ status: 'not ready' });
  }

  try {
    await db.query('SELECT 1');
    await redis.ping();
    res.status(200).json({ status: 'ready' });
  } catch (err) {
    res.status(503).json({ status: 'dependency unavailable', err: err.message });
  }
});

// Graceful shutdown — the pattern that prevents dropped requests during deploys
const server = app.listen(3000, () => {
  isReady = true;
  logger.info('server ready');
});

async function shutdown(signal) {
  logger.info({ signal }, 'shutting down');
  isShuttingDown = true;

  // Stop accepting new requests
  server.close(async () => {
    // Wait for in-flight requests to finish (with a timeout)
    await Promise.race([
      waitForInflightRequests(),
      new Promise((r) => setTimeout(r, 25_000))
    ]);

    // Close dependencies
    await db.destroy();
    await redis.quit();

    logger.info('shutdown complete');
    process.exit(0);
  });

  // Force exit after 30s
  setTimeout(() => {
    logger.error('forced shutdown after timeout');
    process.exit(1);
  }, 30_000).unref();
}

process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));

05 · Security for JavaScript Services

Month 5. A researcher reported a vulnerability in one of Nadia's APIs. A user could access another user's data by changing an ID in the URL. Classic IDOR (Insecure Direct Object Reference).

"The code was clean," she said, "but it didn't check ownership."

Anwar nodded. "Security isn't one feature. It's a habit — of assuming every input is hostile, every user could be an attacker, and every check must be explicit."

The Security Checklist for Every Node.js Service

🛡️

Helmet

HTTP security headers out of the box. Prevents clickjacking, XSS, MIME confusion.

🚦

Rate Limiting

Prevent brute force and abuse. Sliding window in Redis for multi-server setups.

🔐

Input Validation

Zod or Joi at every boundary. Trust nothing from the outside world.

🔑

Auth & AuthZ

JWT or sessions. Always check resource ownership on every read and write.

🔒

Secrets Management

Never commit secrets. Use environment variables + a vault (AWS Secrets Manager, Vault).

🌐

CORS

Explicit allowlist. Never origin: '*' for authenticated APIs.

📦

Dependency Audit

npm audit in CI. Dependabot/Renovate for automated updates.

🚨

Error Sanitization

Never leak stack traces or internal details to clients in production.

The Security Middleware Stack

security.js — production-grade setup JavaScript
import helmet from 'helmet';
import cors from 'cors';
import rateLimit from 'express-rate-limit';
import { RedisStore } from 'rate-limit-redis';

// 1. HTTP security headers
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", 'data:', 'https:'],
      connectSrc: ["'self'"]
    }
  },
  hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' }
}));

// 2. CORS — explicit allowlist, never wildcard for APIs with cookies
const allowedOrigins = [
  'https://app.example.com',
  'https://admin.example.com',
  ...(process.env.NODE_ENV === 'development' ? ['http://localhost:3000'] : [])
];

app.use(cors({
  origin: (origin, cb) => {
    if (!origin) return cb(null, true);  // same-origin / server-to-server
    if (allowedOrigins.includes(origin)) return cb(null, true);
    cb(new Error('Not allowed by CORS'));
  },
  credentials: true,
  maxAge: 86400
}));

// 3. Global rate limit (per IP)
const globalLimiter = rateLimit({
  windowMs: 60_000,
  max: 100,  // 100 requests per minute per IP
  standardHeaders: true,
  legacyHeaders: false,
  // Use Redis so limits work across multiple servers
  store: new RedisStore({ sendCommand: (...args) => redis.sendCommand(args) }),
  message: { error: 'Too many requests, please slow down' }
});

app.use('/api/', globalLimiter);

// 4. Stricter limits for sensitive endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60_000,
  max: 5,  // only 5 login attempts per 15 min
  skipSuccessfulRequests: true,  // successful logins don't count
  store: new RedisStore({ sendCommand: (...args) => redis.sendCommand(args) })
});

app.use('/auth/login', authLimiter);
app.use('/auth/register', authLimiter);
app.use('/auth/forgot-password', authLimiter);

// 5. Body size limits — prevent DoS via huge payloads
app.use(express.json({ limit: '100kb' }));
app.use(express.urlencoded({ extended: false, limit: '100kb' }));

// 6. Prevent HTTP parameter pollution
app.use((req, res, next) => {
  // Reject query params that are arrays when expecting strings
  for (const key in req.query) {
    if (Array.isArray(req.query[key])) {
      return res.status(400).json({ error: `Duplicate query parameter: ${key}` });
    }
  }
  next();
});

Authentication & the IDOR Bug

Nadia's bug: users could access other users' orders. The code authenticated (verified who you are) but didn't authorize (verified you should have access).

idor-fix.js JavaScript
// ❌ VULNERABLE — checks authentication but not authorization
app.get('/api/orders/:id', requireAuth, async (req, res) => {
  const order = await orderRepo.findById(req.params.id);
  if (!order) return res.status(404).json({ error: 'not found' });
  res.json(order);  // 💥 anyone can read any order
});

// ✅ FIXED — authorization at the data layer
app.get('/api/orders/:id', requireAuth, async (req, res) => {
  // Query scoped to the authenticated user — enforced by the data layer
  const order = await orderRepo.findByIdForUser(req.params.id, req.user.id);
  if (!order) {
    // Return 404 (not 403) — don't reveal whether the resource exists
    return res.status(404).json({ error: 'not found' });
  }
  res.json(toOrderResponse(order));
});

// In the repository — authorization is a WHERE clause, not an if statement
async findByIdForUser(orderId, userId) {
  return this.db.queryOne(
    'SELECT * FROM orders WHERE id = $1 AND user_id = $2',
    [orderId, userId]
  );
  // ✓ SQL enforces ownership — impossible to leak another user's order
}
🚨

The IDOR pattern is the #1 API security bug. Every read and write endpoint that takes an ID must verify that the authenticated user owns the resource — or has explicit permission. The cleanest way: enforce it in the query, not in a separate if statement that someone might forget.

06 · 🏗️ Capstone 1: URL Shortener API

Day 1 of capstone week. Anwar gave the team three projects. "Start with the URL shortener," he said. "It sounds simple, but it teaches you every fundamental of API design: persistence, caching, analytics, rate limiting, and testing. Get this right and you understand ninety percent of what production APIs are."

1

URL Shortener API

Beginner–Intermediate ~4 hours 6 endpoints

Build a production-ready URL shortener with custom slugs, click analytics, expiry, rate limiting, Redis caching, and comprehensive tests.

What You'll Build

Express server + project structure
PostgreSQL schema + migrations
Create / redirect / stats endpoints
Redis caching layer
Rate limiting + input validation
Unit + integration tests
Docker + deployment ready

Project Structure

url-shortener/ — folder layout Text
url-shortener/
├── src/
│   ├── server.js                 # entry point
│   ├── app.js                    # Express app (testable, no listen)
│   ├── config/
│   │   └── index.js              # env parsing + validation
│   ├── db/
│   │   ├── pool.js               # pg connection pool
│   │   └── migrations/
│   │       └── 001_init.sql
│   ├── repositories/
│   │   └── url-repository.js     # data access
│   ├── services/
│   │   ├── shortener-service.js  # business logic
│   │   └── analytics-service.js
│   ├── routes/
│   │   ├── urls.js
│   │   └── health.js
│   ├── middleware/
│   │   ├── auth.js
│   │   ├── rate-limit.js
│   │   ├── error-handler.js
│   │   └── request-context.js
│   ├── schemas/
│   │   └── url-schemas.js        # Zod schemas
│   └── lib/
│       ├── logger.js
│       ├── metrics.js
│       └── slug.js               # base62 encoding
├── tests/
│   ├── unit/
│   └── integration/
├── Dockerfile
├── docker-compose.yml
├── package.json
└── .env.example

The Slug Generator — Base62

src/lib/slug.js JavaScript
const ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
const BASE = ALPHABET.length;  // 62

/**
 * Convert a numeric ID to a short base62 string.
 *
 * Why base62?
 *   - URL-safe (no special characters)
 *   - Case-sensitive (more combinations per character)
 *   - Compact (7 chars = 62^7 = 3.5 trillion unique slugs)
 *
 * @param {number} num - Numeric ID from the database
 * @returns {string} Base62-encoded string
 */
export function encodeBase62(num) {
  if (num === 0) return ALPHABET[0];
  let result = '';
  while (num > 0) {
    result = ALPHABET[num % BASE] + result;
    num = Math.floor(num / BASE);
  }
  return result;
}

export function decodeBase62(str) {
  let num = 0;
  for (const char of str) {
    num = num * BASE + ALPHABET.indexOf(char);
  }
  return num;
}

// Test
// encodeBase62(1)      → '1'
// encodeBase62(62)     → '10'
// encodeBase62(3844)   → '100'
// encodeBase62(123456) → 'W7E'

The Core Service

src/services/shortener-service.js JavaScript
import { encodeBase62 } from '../lib/slug.js';
import { getLogger } from '../lib/logger.js';

const RESERVED_SLUGS = new Set([
  'api', 'admin', 'health', 'metrics', 'login', 'logout',
  'register', 'signup', 'pricing', 'about', 'contact', 'terms', 'privacy'
]);

export class ShortenerService {
  #repo;
  #cache;
  #baseUrl;

  constructor({ urlRepository, cache, baseUrl }) {
    this.#repo = urlRepository;
    this.#cache = cache;
    this.#baseUrl = baseUrl;
  }

  async createShortUrl({ longUrl, customSlug, expiresAt, userId, metadata }) {
    const log = getLogger();

    // Validate custom slug
    if (customSlug) {
      if (RESERVED_SLUGS.has(customSlug.toLowerCase())) {
        throw new SlugReservedError(customSlug);
      }
      const existing = await this.#repo.findBySlug(customSlug);
      if (existing) throw new SlugTakenError(customSlug);
    }

    // Insert with a temporary placeholder, then update with base62 slug
    // (This is the classic "two-phase slug generation" pattern)
    const url = await this.#repo.insert({
      longUrl,
      slug: customSlug ?? '__pending__',
      expiresAt,
      userId,
      metadata
    });

    if (!customSlug) {
      const slug = encodeBase62(url.id);
      await this.#repo.updateSlug(url.id, slug);
      url.slug = slug;
    }

    log.info({ urlId: url.id, slug: url.slug, userId }, 'short url created');

    return {
      id: url.id,
      shortUrl: `${this.#baseUrl}/${url.slug}`,
      longUrl: url.longUrl,
      slug: url.slug,
      expiresAt: url.expiresAt,
      createdAt: url.createdAt
    };
  }

  async resolveUrl(slug, { ip, userAgent, referrer } = {}) {
    const log = getLogger();

    // 1. Check cache first (99% of requests hit this)
    let cached = await this.#cache.get(`url:${slug}`);
    if (cached) {
      log.debug({ slug, hit: true }, 'cache hit');
      this.#repo.recordClick({ urlId: cached.id, ip, userAgent, referrer })
        .catch((err) => log.error({ err, slug }, 'failed to record click'));
      return cached;
    }

    // 2. Cache miss — hit the database
    log.debug({ slug, hit: false }, 'cache miss');
    const url = await this.#repo.findBySlug(slug);

    if (!url) {
      throw new NotFoundError('Short URL not found');
    }

    // 3. Check expiry
    if (url.expiresAt && new Date(url.expiresAt) < new Date()) {
      throw new GoneError('Short URL has expired');
    }

    // 4. Populate cache with TTL
    const ttl = url.expiresAt
      ? Math.min(3600, Math.floor((new Date(url.expiresAt) - new Date()) / 1000))
      : 3600;
    await this.#cache.set(`url:${slug}`, url, ttl);

    // 5. Record click asynchronously (don't block the redirect)
    this.#repo.recordClick({ urlId: url.id, ip, userAgent, referrer })
      .catch((err) => log.error({ err, slug }, 'failed to record click'));

    return url;
  }

  async getStats(slug, userId) {
    const url = await this.#repo.findBySlugForUser(slug, userId);
    if (!url) throw new NotFoundError('Short URL not found');

    const [clicksByDay, topReferrers, topCountries] = await Promise.all([
      this.#repo.clicksByDay(url.id, 30),
      this.#repo.topReferrers(url.id, 10),
      this.#repo.topCountries(url.id, 10)
    ]);

    return {
      slug: url.slug,
      totalClicks: url.clickCount,
      clicksByDay,
      topReferrers,
      topCountries,
      createdAt: url.createdAt,
      expiresAt: url.expiresAt
    };
  }
}

The Database Schema

src/db/migrations/001_init.sql SQL
CREATE TABLE urls (
  id BIGSERIAL PRIMARY KEY,
  slug VARCHAR(20) UNIQUE NOT NULL,
  long_url TEXT NOT NULL,
  user_id BIGINT,
  expires_at TIMESTAMPTZ,
  click_count BIGINT DEFAULT 0,
  metadata JSONB DEFAULT '{}',
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_urls_slug ON urls(slug);
CREATE INDEX idx_urls_user_id ON urls(user_id) WHERE user_id IS NOT NULL;
CREATE INDEX idx_urls_expires_at ON urls(expires_at) WHERE expires_at IS NOT NULL;

CREATE TABLE clicks (
  id BIGSERIAL PRIMARY KEY,
  url_id BIGINT REFERENCES urls(id) ON DELETE CASCADE,
  ip_address INET,
  user_agent TEXT,
  referrer TEXT,
  country_code CHAR(2),
  clicked_at TIMESTAMPTZ DEFAULT NOW()
);

-- Partition by month for scale (millions of rows)
CREATE INDEX idx_clicks_url_id_clicked_at ON clicks(url_id, clicked_at DESC);

-- Materialized view for fast analytics
CREATE MATERIALIZED VIEW url_daily_clicks AS
SELECT
  url_id,
  DATE_TRUNC('day', clicked_at) AS day,
  COUNT(*) AS clicks
FROM clicks
GROUP BY url_id, DATE_TRUNC('day', clicked_at);

CREATE UNIQUE INDEX idx_url_daily_clicks ON url_daily_clicks(url_id, day);

The Express Routes

src/routes/urls.js JavaScript
import { Router } from 'express';
import { z } from 'zod';
import { validate } from '../middleware/validate.js';
import { requireAuth } from '../middleware/auth.js';
import { asyncHandler } from '../middleware/async-handler.js';

const CreateUrlSchema = z.object({
  longUrl: z.string().url().max(2048),
  customSlug: z.string().regex(/^[a-zA-Z0-9_-]{3,20}$/).optional(),
  expiresInDays: z.number().int().min(1).max(3650).optional(),
  metadata: z.record(z.string(), z.unknown()).optional()
});

export function createUrlRoutes({ shortenerService }) {
  const router = Router();

  // POST /api/urls — create a short URL
  router.post('/',
    requireAuth,
    validate(CreateUrlSchema),
    asyncHandler(async (req, res) => {
      const { longUrl, customSlug, expiresInDays, metadata } = req.body;
      const expiresAt = expiresInDays
        ? new Date(Date.now() + expiresInDays * 86_400_000)
        : null;

      const result = await shortenerService.createShortUrl({
        longUrl,
        customSlug,
        expiresAt,
        userId: req.user.id,
        metadata
      });

      res.status(201).json(result);
    })
  );

  // GET /api/urls/:slug/stats
  router.get('/:slug/stats',
    requireAuth,
    asyncHandler(async (req, res) => {
      const stats = await shortenerService.getStats(req.params.slug, req.user.id);
      res.json(stats);
    })
  );

  // DELETE /api/urls/:slug
  router.delete('/:slug',
    requireAuth,
    asyncHandler(async (req, res) => {
      await shortenerService.deleteUrl(req.params.slug, req.user.id);
      res.status(204).end();
    })
  );

  return router;
}

// The public redirect route
export function createRedirectRoute({ shortenerService }) {
  const router = Router();

  router.get('/:slug',
    asyncHandler(async (req, res) => {
      const url = await shortenerService.resolveUrl(req.params.slug, {
        ip: req.ip,
        userAgent: req.headers['user-agent'],
        referrer: req.headers['referer']
      });

      res.redirect(302, url.longUrl);
    })
  );

  return router;
}

The Tests

tests/integration/urls.test.js JavaScript
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import request from 'supertest';
import { createApp } from '../../src/app.js';
import { createTestDb } from '../helpers/test-db.js';

describe('URL shortener API', () => {
  let app, db, token;

  beforeAll(async () => {
    db = await createTestDb();
    app = createApp({ db });
    token = await createTestUser(db);  // returns JWT
  });

  afterAll(async () => await db.destroy());

  beforeEach(async () => {
    await db.query('TRUNCATE urls, clicks RESTART IDENTITY CASCADE');
  });

  describe('POST /api/urls', () => {
    it('creates a short URL with a generated slug', async () => {
      const res = await request(app)
        .post('/api/urls')
        .set('Authorization', `Bearer ${token}`)
        .send({ longUrl: 'https://example.com/very/long/path' });

      expect(res.status).toBe(201);
      expect(res.body).toMatchObject({
        slug: expect.any(String),
        shortUrl: expect.stringContaining(res.body.slug),
        longUrl: 'https://example.com/very/long/path'
      });
    });

    it('rejects invalid URLs', async () => {
      const res = await request(app)
        .post('/api/urls')
        .set('Authorization', `Bearer ${token}`)
        .send({ longUrl: 'not-a-url' });

      expect(res.status).toBe(400);
      expect(res.body.error).toContain('url');
    });

    it('rejects reserved custom slugs', async () => {
      const res = await request(app)
        .post('/api/urls')
        .set('Authorization', `Bearer ${token}`)
        .send({ longUrl: 'https://example.com', customSlug: 'admin' });

      expect(res.status).toBe(422);
    });

    it('rejects duplicate custom slugs', async () => {
      await request(app)
        .post('/api/urls')
        .set('Authorization', `Bearer ${token}`)
        .send({ longUrl: 'https://a.com', customSlug: 'mylink' });

      const res = await request(app)
        .post('/api/urls')
        .set('Authorization', `Bearer ${token}`)
        .send({ longUrl: 'https://b.com', customSlug: 'mylink' });

      expect(res.status).toBe(409);
    });
  });

  describe('GET /:slug (redirect)', () => {
    it('redirects to the long URL', async () => {
      const createRes = await request(app)
        .post('/api/urls')
        .set('Authorization', `Bearer ${token}`)
        .send({ longUrl: 'https://example.com/target', customSlug: 'test1' });

      const res = await request(app).get('/test1').redirects(0);

      expect(res.status).toBe(302);
      expect(res.headers.location).toBe('https://example.com/target');
    });

    it('returns 404 for unknown slugs', async () => {
      const res = await request(app).get('/nonexistent');
      expect(res.status).toBe(404);
    });

    it('returns 410 for expired URLs', async () => {
      await request(app)
        .post('/api/urls')
        .set('Authorization', `Bearer ${token}`)
        .send({ longUrl: 'https://a.com', customSlug: 'expired', expiresInDays: 1 });

      // Manually expire it
      await db.query("UPDATE urls SET expires_at = NOW() - INTERVAL '1 day' WHERE slug = 'expired'");

      const res = await request(app).get('/expired');
      expect(res.status).toBe(410);
    });
  });

  describe('GET /api/urls/:slug/stats', () => {
    it('returns click statistics for the owner', async () => {
      const create = await request(app)
        .post('/api/urls')
        .set('Authorization', `Bearer ${token}`)
        .send({ longUrl: 'https://a.com', customSlug: 'stats1' });

      // Simulate 3 clicks
      for (let i = 0; i < 3; i++) {
        await request(app).get('/stats1');
      }
      // Wait for async click recording
      await new Promise((r) => setTimeout(r, 100));

      const res = await request(app)
        .get('/api/urls/stats1/stats')
        .set('Authorization', `Bearer ${token}`);

      expect(res.status).toBe(200);
      expect(res.body.totalClicks).toBe(3);
    });

    it('returns 404 for another user\'s URL (IDOR prevention)', async () => {
      // User A creates a URL
      await request(app)
        .post('/api/urls')
        .set('Authorization', `Bearer ${token}`)
        .send({ longUrl: 'https://a.com', customSlug: 'secret' });

      // User B tries to access it
      const otherToken = await createTestUser(db, 'other@example.com');
      const res = await request(app)
        .get('/api/urls/secret/stats')
        .set('Authorization', `Bearer ${otherToken}`);

      expect(res.status).toBe(404);
    });
  });
});

Docker Compose — Run Everything Locally

docker-compose.yml YAML
version: '3.9'
services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: production
      DATABASE_URL: postgres://urlshortener:secret@postgres:5432/urlshortener
      REDIS_URL: redis://redis:6379
      JWT_SECRET: ${JWT_SECRET}
      BASE_URL: http://localhost:3000
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: urlshortener
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: urlshortener
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./src/db/migrations:/docker-entrypoint-initdb.d
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U urlshortener"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes
    volumes:
      - redisdata:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

volumes:
  pgdata:
  redisdata:
🏆

What you've learned building this:

✓ Express structure that's testable without spawning a server.
✓ Base62 encoding — a real algorithm used by bit.ly and TinyURL.
✓ Two-phase slug generation (insert → encode → update).
✓ Cache-aside pattern with Redis and TTL calculated from expiry.
✓ Async click recording (fire-and-forget with error logging).
✓ IDOR prevention via query-level authorization.
✓ Reserved slug protection.
✓ Rate limiting scoped per endpoint.
✓ Zod validation at the boundary.
✓ Integration tests with real HTTP, real DB, and IDOR regression tests.

07 · 🏗️ Capstone 2: Real-Time Collaborative Task Manager

Day 3 of capstone week. "Now the hard one," Anwar said. "Real-time. Multiple users editing the same data at the same time. Presence indicators. Conflict resolution. This is where you learn that synchronous request/response is a special case, not the rule."

2

Real-Time Collaborative Task Manager

Intermediate–Advanced ~8 hours WebSockets + REST

Multi-user boards with live updates, presence indicators, optimistic UI, offline resilience, and conflict resolution using Lamport-style clocks.

Architecture

architecture.txt Diagram
┌─────────────┐    ┌──────────────┐    ┌──────────────┐
│  Browser A  │────│  WebSocket   │────│  Postgres    │
│  (Client)   │    │   Server     │    │              │
└─────────────┘    │  (Socket.io) │    │  - tasks     │
                   │              │    │  - boards    │
┌─────────────┐    │  ┌─────────┐ │    │  - users     │
│  Browser B  │────│  │ Redis   │ │    │  - audit_log │
│  (Client)   │    │  │ Pub/Sub │ │    └──────────────┘
└─────────────┘    │  └─────────┘ │
                   │              │    ┌──────────────┐
┌─────────────┐    │  ┌─────────┐ │────│  REST API    │
│  Browser C  │────│  │Room Mgr │ │    │  (Express)   │
│  (Client)   │    │  └─────────┘ │    └──────────────┘
└─────────────┘    └──────────────┘
       │                                       │
       └─────────── JWT Auth ──────────────────┘

WebSocket Server with Socket.io

src/ws/server.js JavaScript
import { Server as SocketIOServer } from 'socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';
import { verifyToken } from '../lib/jwt.js';
import { getLogger } from '../lib/logger.js';

export async function createWsServer(httpServer, { taskService, presenceService, boardService }) {
  const io = new SocketIOServer(httpServer, {
    cors: { origin: process.env.FRONTEND_URL, credentials: true },
    // Long-polling fallback + upgrade to WS
    transports: ['websocket', 'polling'],
    // Heartbeat
    pingInterval: 25_000,
    pingTimeout: 60_000,
    maxHttpBufferSize: 1e6  // 1 MB max message
  });

  // Redis adapter — allows multi-instance WebSocket servers
  const pubClient = createClient({ url: process.env.REDIS_URL });
  const subClient = pubClient.duplicate();
  await Promise.all([pubClient.connect(), subClient.connect()]);
  io.adapter(createAdapter(pubClient, subClient));

  // Authentication — verify JWT on connection
  io.use(async (socket, next) => {
    try {
      const token = socket.handshake.auth.token;
      if (!token) return next(new Error('Authentication required'));

      const user = await verifyToken(token);
      socket.data.user = user;
      socket.data.sessionId = crypto.randomUUID();

      next();
    } catch (err) {
      next(new Error('Invalid token'));
    }
  });

  io.on('connection', (socket) => {
    const log = getLogger();
    const user = socket.data.user;
    log.info({ userId: user.id, socketId: socket.id }, 'ws connected');

    // ─────────────────────────────────────────────────
    // Board room management
    // ─────────────────────────────────────────────────

    socket.on('board:join', async ({ boardId }, cb) => {
      try {
        // Authorization — verify user has access to this board
        const hasAccess = await boardService.userHasAccess(boardId, user.id);
        if (!hasAccess) return cb({ error: 'Access denied' });

        const room = `board:${boardId}`;
        socket.join(room);
        socket.data.boardId = boardId;

        // Track presence
        await presenceService.addUser(boardId, {
          userId: user.id,
          sessionId: socket.data.sessionId,
          name: user.name,
          avatarUrl: user.avatarUrl,
          joinedAt: new Date().toISOString()
        });

        // Broadcast to room that someone joined
        socket.to(room).emit('presence:joined', {
          userId: user.id,
          name: user.name,
          avatarUrl: user.avatarUrl,
          sessionId: socket.data.sessionId
        });

        // Send current state to the joining user
        const [tasks, presence] = await Promise.all([
          taskService.listForBoard(boardId),
          presenceService.listForBoard(boardId)
        ]);

        cb({ tasks, presence });
      } catch (err) {
        log.error({ err, boardId }, 'board:join failed');
        cb({ error: 'Failed to join board' });
      }
    });

    // ─────────────────────────────────────────────────
    // Task mutations with optimistic concurrency
    // ─────────────────────────────────────────────────

    socket.on('task:update', async ({ taskId, patch, clientTimestamp }, cb) => {
      const boardId = socket.data.boardId;
      if (!boardId) return cb({ error: 'Not in a board' });

      try {
        // Use a Lamport clock for ordering events across clients
        const lamport = nextLamport(boardId, clientTimestamp);

        // Update with version check — prevents lost updates
        const result = await taskService.updateWithVersion({
          taskId,
          patch,
          expectedVersion: patch.version,
          userId: user.id,
          lamport
        });

        if (result.conflict) {
          // Server wins — client must reconcile
          return cb({
            conflict: true,
            current: result.current,
            message: 'Task was modified by another user'
          });
        }

        // Broadcast to everyone in the room (including the sender for confirmation)
        io.to(`board:${boardId}`).emit('task:updated', {
          task: result.task,
          by: { userId: user.id, name: user.name },
          lamport
        });

        cb({ ok: true, task: result.task });
      } catch (err) {
        log.error({ err, taskId }, 'task:update failed');
        cb({ error: 'Update failed' });
      }
    });

    socket.'task:create', async ({ title, description, status }, cb) => {
      const boardId = socket.data.boardId;
      if (!boardId) return cb({ error: 'Not in a board' });

      try {
        const task = await taskService.create({
          boardId,
          title,
          description,
          status: status ?? 'todo',
          createdBy: user.id
        });

        io.to(`board:${boardId}`).emit('task:created', {
          task,
          by: { userId: user.id, name: user.name }
        });

        cb({ ok: true, task });
      } catch (err) {
        cb({ error: err.message });
      }
    });

    // ─────────────────────────────────────────────────
    // Typing indicators — ephemeral, not persisted
    // ─────────────────────────────────────────────────

    socket.on('task:typing', ({ taskId, isTyping }) => {
      const boardId = socket.data.boardId;
      if (!boardId) return;

      socket.to(`board:${boardId}`).emit('task:typing', {
        taskId,
        userId: user.id,
        name: user.name,
        isTyping
      });
    });

    // Cursor position — for future collaborative editing
    socket.on('cursor:move', ({ x, y }) => {
      const boardId = socket.data.boardId;
      if (!boardId) return;
      socket.to(`board:${boardId}`).emit('cursor:moved', { userId: user.id, x, y });
    });

    // ─────────────────────────────────────────────────
    // Disconnect — clean up presence
    // ─────────────────────────────────────────────────

    socket.on('disconnect', async (reason) => {
      const boardId = socket.data.boardId;
      log.info({ userId: user.id, boardId, reason }, 'ws disconnected');

      if (boardId) {
        await presenceService.removeUser(boardId, socket.data.sessionId);
        io.to(`board:${boardId}`).emit('presence:left', {
          userId: user.id,
          sessionId: socket.data.sessionId
        });
      }
    });
  });

  return io;
}

// Lamport clock — monotonic across nodes via Redis
async function nextLamport(boardId, clientTs = 0) {
  const key = `lamport:${boardId}`;
  const serverTime = Date.now();
  const next = Math.max(serverTime, clientTs) + 1;
  await redis.set(key, next, { EX: 86400 });
  return next;
}

Optimistic Concurrency in the Repository

src/repositories/task-repository.js JavaScript
export class TaskRepository {
  #db;
  constructor({ db }) { this.#db = db; }

  async updateWithVersion({ taskId, patch, expectedVersion, userId, lamport }) {
    // Atomic compare-and-swap at the SQL level
    // This is the ONLY place we need to worry about races — the DB handles it
    const result = await this.#db.query(
      `
        UPDATE tasks
        SET
          title = COALESCE($1, title),
          description = COALESCE($2, description),
          status = COALESCE($3, status),
          version = version + 1,
          updated_at = NOW(),
          updated_by = $4,
          lamport = $5
        WHERE id = $6 AND version = $7
        RETURNING *
      `,
      [patch.title, patch.description, patch.status, userId, lamport, taskId, expectedVersion]
    );

    if (result.rowCount === 0) {
      // Version mismatch — someone else updated first
      const current = await this.#db.queryOne('SELECT * FROM tasks WHERE id = $1', [taskId]);
      return { conflict: true, current };
    }

    // Audit log — who changed what, when
    await this.#db.query(
      `
        INSERT INTO task_audit (task_id, user_id, change, lamport, occurred_at)
        VALUES ($1, $2, $3, $4, NOW())
      `,
      [taskId, userId, JSON.stringify(patch), lamport]
    );

    return { task: result.rows[0] };
  }
}

Presence Service with Redis

src/services/presence-service.js JavaScript
export class PresenceService {
  #redis;
  #ttlSeconds = 60;  // Sessions expire if client stops heartbeating

  constructor({ redis }) { this.#redis = redis; }

  async addUser(boardId, userSession) {
    const key = `presence:${boardId}`;
    await this.#redis.hSet(key, userSession.sessionId, JSON.stringify(userSession));
    await this.#redis.expire(key, this.#ttlSeconds * 10);
  }

  async removeUser(boardId, sessionId) {
    await this.#redis.hDel(`presence:${boardId}`, sessionId);
  }

  async heartbeat(boardId, sessionId) {
    // Keep the presence alive
    const key = `presence:${boardId}`;
    const userData = await this.#redis.hGet(key, sessionId);
    if (userData) {
      const parsed = JSON.parse(userData);
      parsed.lastSeen = new Date().toISOString();
      await this.#redis.hSet(key, sessionId, JSON.stringify(parsed));
    }
  }

  async listForBoard(boardId) {
    const raw = await this.#redis.hGetAll(`presence:${boardId}`);
    return Object.values(raw).map(JSON.parse);
  }
}

The Client — Optimistic UI + Reconnection

client/board-client.js — frontend side JavaScript
import { io } from 'socket.io-client';

export class BoardClient {
  #socket;
  #pendingUpdates = new Map();  // taskId → optimistic version
  #listeners = new Map();

  connect(token, boardId) {
    this.#socket = io('wss://api.example.com', {
      auth: { token },
      reconnection: true,
      reconnectionAttempts: Infinity,
      reconnectionDelay: 1000,
      reconnectionDelayMax: 30_000
    });

    this.#socket.on('connect', () => {
      this.#socket.emit('board:join', { boardId }, (response) => {
        if (response.error) {
          this.#emit('error', response.error);
          return;
        }
        this.#emit('snapshot', response);
        this.#startHeartbeat(boardId);
      });
    });

    this.#socket.on('task:updated', ({ task, by }) => {
      // If this was our optimistic update, confirm it
      if (this.#pendingUpdates.has(task.id)) {
        this.#pendingUpdates.delete(task.id);
        this.#emit('task:confirmed', { task });
      } else {
        // Someone else updated it
        this.#emit('task:remote-update', { task, by });
      }
    });

    this.#socket.on('presence:joined', (user) => this.#emit('presence:joined', user));
    this.#socket.on('presence:left', (user) => this.#emit('presence:left', user));

    this.#socket.on('task:created', ({ task }) => this.#emit('task:created', { task }));
    this.#socket.on('task:typing', (data) => this.#emit('task:typing', data));
  }

  updateTask(taskId, patch, currentVersion) {
    // OPTIMISTIC — apply locally immediately
    this.#pendingUpdates.set(taskId, currentVersion);
    this.#emit('task:optimistic', { taskId, patch });

    // Send to server
    this.#socket.emit('task:update', {
      taskId,
      patch: { ...patch, version: currentVersion },
      clientTimestamp: Date.now()
    }, (response) => {
      if (response.conflict) {
        // Server has newer version — roll back optimistic update
        this.#pendingUpdates.delete(taskId);
        this.#emit('task:conflict', {
          taskId,
          attempted: patch,
          current: response.current,
          message: response.message
        });
      } else if (response.error) {
        this.#pendingUpdates.delete(taskId);
        this.#emit('task:error', { taskId, error: response.error });
      }
    });
  }

  #startHeartbeat(boardId) {
    this.#heartbeatId = setInterval(() => {
      this.#socket.emit('presence:heartbeat', { boardId });
    }, 20_000);
  }

  #emit(event, data) {
    const handlers = this.#listeners.get(event) ?? [];
    handlers.forEach((h) => h(data));
  }

  on(event, handler) {
    if (!this.#listeners.has(event)) this.#listeners.set(event, []);
    this.#listeners.get(event).push(handler);
    return () => {
      const handlers = this.#listeners.get(event);
      const idx = handlers.indexOf(handler);
      if (idx >= 0) handlers.splice(idx, 1);
    };
  }

  disconnect() {
    if (this.#heartbeatId) clearInterval(this.#heartbeatId);
    this.#socket?.disconnect();
  }
}

Interactive: Watch Real-Time Updates Flow

Interactive — Real-Time Multi-User Simulation
🏆

What you've learned building this:

✓ WebSocket server with JWT authentication.
✓ Room-based broadcast architecture.
✓ Redis Pub/Sub for horizontal scaling.
✓ Presence tracking with Redis hashes and TTLs.
✓ Optimistic UI updates with rollback on conflict.
✓ Lamport clocks for cross-client ordering.
✓ Version-based optimistic concurrency (compare-and-swap in SQL).
✓ Audit logging of every change.
✓ Graceful reconnection with exponential backoff.
✓ Heartbeat-based presence detection.

08 · 🏗️ Capstone 3: AI Content Moderation Service

Day 6 of capstone week. "The last one," Anwar said, "and the most modern. You're building a service that receives user content, sends it to an AI model for moderation, and returns a verdict. The challenges: rate limits, costs, latency, streaming responses, and graceful degradation when the AI is slow or down."

"This is what production JavaScript looks like in 2026. Async everywhere. Third-party APIs you don't control. Costs you must manage. And a system that must keep working when any individual piece fails."

3

AI Content Moderation Service

Advanced ~10 hours Queue + Streaming

Queue-based content moderation with Claude API, streaming responses, per-tenant rate limits, cost tracking, circuit breakers, and graceful fallback to heuristic rules.

Architecture

architecture.txt Diagram
                    ┌──────────────────┐
    HTTP Request ──▶│  Express API     │
                    │  (validate +     │
                    │   enqueue)       │
                    └────────┬─────────┘
                             │
                             ▼
                    ┌──────────────────┐
                    │  Redis Queue     │   ◀── Priority-based
                    │  (BullMQ)        │       + retry + backoff
                    └────────┬─────────┘
                             │
                             ▼
                    ┌──────────────────┐
                    │  Worker Pool     │   ◀── Concurrent workers
                    │  (N processes)   │       across N containers
                    └────────┬─────────┘
                             │
                ┌────────────┼────────────┐
                ▼            ▼            ▼
        ┌──────────┐  ┌──────────┐  ┌──────────┐
        │ Claude   │  │ Heuristic│  │ Postgres │
        │ API      │  │ Fallback │  │ (results)│
        └──────────┘  └──────────┘  └──────────┘
                                            │
                                            ▼
                                   ┌──────────────┐
                                   │  Webhook /   │
                                   │  Polling API │
                                   └──────────────┘

The API — Enqueue and Return Immediately

src/routes/moderate.js JavaScript
import { Router } from 'express';
import { z } from 'zod';

const ModerateSchema = z.object({
  content: z.string().min(1).max(10_000),
  contentType: z.enum(['text', 'html', 'markdown']).default('text'),
  callbackUrl: z.string().url().optional(),
  priority: z.enum(['low', 'normal', 'high']).default('normal'),
  tenantId: z.string()
});

export function createModerateRoutes({ moderationService }) {
  const router = Router();

  // Async endpoint — returns job ID immediately
  router.post('/jobs',
    validate(ModerateSchema),
    asyncHandler(async (req, res) => {
      const job = await moderationService.enqueue({
        ...req.body,
        tenantId: req.user.tenantId,
        submittedBy: req.user.id
      });

      res.status(202).json({
        jobId: job.id,
        status: 'queued',
        estimatedWaitMs: await moderationService.estimateWait(req.body.priority),
        pollUrl: `/api/moderate/jobs/${job.id}`
      });
    })
  );

  // Poll for result
  router.get('/jobs/:id',
    asyncHandler(async (req, res) => {
      const result = await moderationService.getJob(req.params.id, req.user.tenantId);
      if (!result) return res.status(404).json({ error: 'Job not found' });
      res.json(result);
    })
  );

  // Streaming endpoint — real-time moderation for interactive use
  router.post('/stream',
    validate(ModerateSchema),
    async (req, res) => {
      res.setHeader('Content-Type', 'text/event-stream');
      res.setHeader('Cache-Control', 'no-cache');
      res.setHeader('Connection', 'keep-alive');
      res.flushHeaders();

      const controller = new AbortController();
      req.on('close', () => controller.abort());

      try {
        for await (const event of moderationService.stream(req.body, { signal: controller.signal })) {
          res.write(`event: ${event.type}\ndata: ${JSON.stringify(event.data)}\n\n`);
        }
        res.end();
      } catch (err) {
        res.write(`event: error\ndata: ${JSON.stringify({ message: err.message })}\n\n`);
        res.end();
      }
    }
  );

  return router;
}

The Moderation Service — Queue + Circuit Breaker

src/services/moderation-service.js JavaScript
import { Queue, Worker } from 'bullmq';
import { CircuitBreaker } from '../lib/circuit-breaker.js';
import { getLogger } from '../lib/logger.js';

export class ModerationService {
  #queue;
  #worker;
  #repo;
  #aiClient;
  #heuristic;
  #breaker;
  #redis;

  constructor({ redis, repo, aiClient, heuristic }) {
    this.#redis = redis;
    this.#repo = repo;
    this.#aiClient = aiClient;
    this.#heuristic = heuristic;

    // Circuit breaker — trips after 5 failures in 60s, retries after 30s
    this.#breaker = new CircuitBreaker({
      failureThreshold: 5,
      successThreshold: 2,
      timeout: 60_000,
      resetTimeout: 30_000
    });

    // BullMQ queue — jobs stored in Redis, workers process them
    this.#queue = new Queue('moderation', {
      connection: redis,
      defaultJobOptions: {
        attempts: 3,
        backoff: { type: 'exponential', delay: 2000 },
        removeOnComplete: { age: 86400, count: 10_000 },
        removeOnFail: { age: 604800 }
      }
    });
  }

  async enqueue({ content, contentType, callbackUrl, priority, tenantId, submittedBy }) {
    // 1. Enforce per-tenant rate limits BEFORE enqueueing
    const allowed = await this.#checkTenantQuota(tenantId);
    if (!allowed) throw new QuotaExceededError(tenantId);

    // 2. Persist job metadata in Postgres (source of truth)
    const job = await this.#repo.createJob({
      tenantId,
      submittedBy,
      contentType,
      callbackUrl,
      priority,
      status: 'queued',
      contentHash: sha256(content)  // for dedup + cache lookup
    });

    // 3. Check cache — same content moderated recently?
    const cached = await this.#repo.findCachedResult(job.contentHash, 3600);
    if (cached) {
      await this.#repo.completeJob(job.id, cached.result, { cached: true });
      return { ...job, status: 'completed' };
    }

    // 4. Enqueue to BullMQ with priority
    await this.#queue.add(
      'moderate',
      { jobId: job.id, content, contentType, tenantId },
      { priority: this.#priorityValue(priority), jobId: job.id }
    );

    return job;
  }

  // Streaming moderation — for interactive use
  async* stream({ content, contentType }, { signal }) {
    yield { type: 'started', data: { contentType, length: content.length } };

    // Stage 1: heuristic pre-filter (fast, no AI)
    const heuristicResult = this.#heuristic.analyze(content);
    yield { type: 'heuristic', data: heuristicResult };

    // Early exit — obvious violations don't need AI
    if (heuristicResult.severity === 'block') {
      yield { type: 'verdict', data: { ...heuristicResult, source: 'heuristic' } };
      return;
    }

    // Stage 2: AI moderation (streaming)
    if (this.#breaker.isOpen()) {
      yield {
        type: 'fallback',
        data: { reason: 'AI circuit breaker open' }
      };
      yield { type: 'verdict', data: { ...heuristicResult, source: 'fallback' } };
      return;
    }

    try {
      const stream = await this.#breaker.execute(() =>
        this.#aiClient.streamModerate(content, { signal })
      );

      let fullText = '';
      for await (const chunk of stream) {
        fullText += chunk.text;
        yield { type: 'ai-chunk', data: { text: chunk.text } };
      }

      const verdict = this.#parseAiVerdict(fullText);
      yield { type: 'verdict', data: { ...verdict, source: 'ai' } };
    } catch (err) {
      yield { type: 'error', data: { message: err.message } };
      yield { type: 'verdict', data: { ...heuristicResult, source: 'fallback' } };
    }
  }

  async getJob(jobId, tenantId) {
    return this.#repo.findJob(jobId, tenantId);
  }

  async estimateWait(priority) {
    const counts = await this.#queue.getJobCounts();
    const base = (counts.waiting ?? 0) * 200;  // 200ms avg per job
    const multiplier = { high: 0.3, normal: 1, low: 3 }[priority] ?? 1;
    return Math.round(base * multiplier);
  }

  startWorker() {
    this.#worker = new Worker('moderation', async (job) => {
      const log = getLogger();
      const { jobId, content, tenantId } = job.data;

      try {
        await this.#repo.markProcessing(jobId);

        // Try AI with circuit breaker
        let verdict;
        if (this.#breaker.isOpen()) {
          log.warn({ jobId }, 'circuit breaker open, using fallback');
          verdict = { ...this.#heuristic.analyze(content), source: 'fallback' };
        } else {
          const aiResult = await this.#breaker.execute(() =>
            this.#aiClient.moderate(content)
          );
          verdict = { ...aiResult, source: 'ai' };
        }

        await this.#repo.completeJob(jobId, verdict, { cached: false });

        // Fire webhook if provided
        const meta = await this.#repo.findJob(jobId, tenantId);
        if (meta?.callbackUrl) {
          await this.#deliverWebhook(meta.callbackUrl, { jobId, verdict }).catch((err) =>
            log.error({ err, jobId }, 'webhook delivery failed')
          );
        }

        return verdict;
      } catch (err) {
        log.error({ err, jobId }, 'job failed');
        await this.#repo.failJob(jobId, err.message);
        throw err;  // BullMQ will retry
      }
    }, {
      connection: this.#redis,
      concurrency: 10,
      limiter: {
        max: 100,      // max 100 jobs per duration across the pool
        duration: 60_000
      }
    });

    this.#worker.on('completed', (job) => {
      getLogger().info({ jobId: job.id }, 'job completed');
    });

    this.#worker.on('failed', (job, err) => {
      getLogger().error({ jobId: job?.id, err: err.message, attemptsMade: job?.attemptsMade }, 'job failed permanently');
    });
  }

  async #checkTenantQuota(tenantId) {
    const key = `quota:${tenantId}:${new Date().toISOString().slice(0, 13)}`;  // hourly bucket
    const current = await this.#redis.incr(key);
    if (current === 1) await this.#redis.expire(key, 7200);
    return current <= this.#getTenantLimit(tenantId);
  }

  #getTenantLimit(tenantId) {
    // In production, look this up from a tenant table
    return 1000;  // 1000 moderation jobs per hour per tenant
  }

  #priorityValue(priority) {
    // Lower number = higher priority in BullMQ
    return { high: 1, normal: 5, low: 10 }[priority] ?? 5;
  }

  async #deliverWebhook(url, payload) {
    const res = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Signature': hmacSha256(JSON.stringify(payload), process.env.WEBHOOK_SECRET)
      },
      body: JSON.stringify(payload),
      signal: AbortSignal.timeout(10_000)
    });
    if (!res.ok) throw new Error(`Webhook returned ${res.status}`);
  }

  #parseAiVerdict(text) {
    // The AI returns structured JSON — parse and validate with Zod
    const match = text.match(/\{[\s\S]*\}/);
    if (!match) throw new Error('AI response missing JSON');

    const parsed = JSON.parse(match[0]);
    return VerdictSchema.parse(parsed);
  }
}

The Circuit Breaker

src/lib/circuit-breaker.js JavaScript
/**
 * Circuit Breaker — prevents cascading failures when a dependency is down.
 *
 * Three states:
 *   CLOSED    — everything working, calls pass through
 *   OPEN      — too many failures, calls fail fast (don't even try)
 *   HALF_OPEN — testing recovery, allow a few calls through
 */
export class CircuitBreaker {
  #state = 'CLOSED';
  #failureCount = 0;
  #successCount = 0;
  #openedAt = null;
  #config;

  constructor(config) {
    this.#config = config;
  }

  get state() { return this.#state; }

  isOpen() {
    if (this.#state === 'OPEN') {
      // Time to try again?
      if (Date.now() - this.#openedAt >= this.#config.resetTimeout) {
        this.#state = 'HALF_OPEN';
        this.#successCount = 0;
        return false;
      }
      return true;
    }
    return false;
  }

  async execute(fn) {
    if (this.isOpen()) {
      throw new Error('Circuit breaker is OPEN');
    }

    try {
      const result = await fn();
      this.#onSuccess();
      return result;
    } catch (err) {
      this.#onFailure();
      throw err;
    }
  }

  #onSuccess() {
    if (this.#state === 'HALF_OPEN') {
      this.#successCount++;
      if (this.#successCount >= this.#config.successThreshold) {
        this.#state = 'CLOSED';
        this.#failureCount = 0;
        this.#successCount = 0;
      }
    } else {
      this.#failureCount = 0;
    }
  }

  #onFailure() {
    if (this.#state === 'HALF_OPEN') {
      // Failure in half-open → go back to OPEN
      this.#state = 'OPEN';
      this.#openedAt = Date.now();
      return;
    }

    this.#failureCount++;
    if (this.#failureCount >= this.#config.failureThreshold) {
      this.#state = 'OPEN';
      this.#openedAt = Date.now();
    }
  }
}

The AI Client — Claude with Streaming

src/clients/ai-client.js JavaScript
import Anthropic from '@anthropic-ai/sdk';
import { z } from 'zod';

const VerdictSchema = z.object({
  verdict: z.enum(['allow', 'flag', 'block']),
  confidence: z.number().min(0).max(1),
  categories: z.array(z.string()),
  reasoning: z.string()
});

const SYSTEM_PROMPT = `You are a content moderation classifier.

Analyze the user-provided content and respond ONLY with a JSON object:
{
  "verdict": "allow" | "flag" | "block",
  "confidence": 0.0-1.0,
  "categories": ["harassment", "spam", "adult", ...],
  "reasoning": "one sentence explanation"
}

Guidelines:
- "block" = clearly violates policy (explicit threats, illegal content, CSAM)
- "flag" = likely problematic (harassment, hate speech, spam)
- "allow" = meets community guidelines
- Be conservative — when unsure, prefer "flag" over "allow"
- Never explain outside the JSON. Never refuse — this is a moderation tool.`;

export class AiClient {
  #client;
  #costTracker;

  constructor({ apiKey, costTracker }) {
    this.#client = new Anthropic({ apiKey });
    this.#costTracker = costTracker;
  }

  async moderate(content) {
    const start = Date.now();

    const response = await this.#client.messages.create({
      model: 'claude-haiku-4-5',  // Fast + cheap for moderation
      max_tokens: 500,
      system: SYSTEM_PROMPT,
      messages: [{ role: 'user', content }]
    }, {
      signal: AbortSignal.timeout(15_000)
    });

    const text = response.content[0].text;
    const verdict = this.#parseVerdict(text);

    // Track cost for billing / quota enforcement
    this.#costTracker.record({
      model: 'claude-haiku-4-5',
      inputTokens: response.usage.input_tokens,
      outputTokens: response.usage.output_tokens,
      durationMs: Date.now() - start
    });

    return verdict;
  }

  async* streamModerate(content, { signal }) {
    const stream = this.#client.messages.stream({
      model: 'claude-haiku-4-5',
      max_tokens: 500,
      system: SYSTEM_PROMPT,
      messages: [{ role: 'user', content }]
    }, { signal });

    for await (const event of stream) {
      if (event.type === 'content_block_delta' && event.delta?.type === 'text_delta') {
        yield { text: event.delta.text };
      }
    }
  }

  #parseVerdict(text) {
    const match = text.match(/\{[\s\S]*\}/);
    if (!match) throw new Error('No JSON in AI response');
    return VerdictSchema.parse(JSON.parse(match[0]));
  }
}

Interactive: Watch the Circuit Breaker in Action

Interactive — Circuit Breaker States
🏆

What you've learned building this:

✓ BullMQ queue architecture with priorities, retries, exponential backoff.
✓ Worker pool with rate limiting and concurrency control.
✓ Circuit breaker pattern for external dependency failures.
✓ Heuristic fallback when AI is unavailable.
✓ Streaming responses via Server-Sent Events.
✓ Content-hash caching to avoid duplicate moderation costs.
✓ Per-tenant rate limiting with Redis counters.
✓ Webhook delivery with HMAC signatures and timeouts.
✓ Cost tracking for AI usage (essential for business viability).
✓ Structured logging + metrics across all components.

09 · The Complete Developer's Toolkit

After seven parts and three capstones, you have a serious technical foundation. Here's the curated toolkit for continuing to grow — the tools, resources, and habits that senior JavaScript engineers rely on.

Essential Daily Tools

Category Tools Why
Runtime Node.js 22 LTS, Bun, Deno Node is the standard. Bun and Deno are worth exploring for specific use cases.
Framework Express, Fastify, Hono, NestJS Express for familiarity. Fastify for perf. Hono for edge. NestJS for large teams.
Database Postgres, Drizzle ORM, Prisma Postgres is the default. Drizzle is type-safe SQL. Prisma for schema-first DX.
Cache / Queue Redis, BullMQ Redis is the Swiss army knife. BullMQ for background jobs.
Validation Zod, Valibot Runtime validation + type inference at the boundaries.
Testing Vitest, Playwright, Supertest Modern standard across all three test layers.
Observability Pino, OpenTelemetry, Prometheus Logs, traces, metrics — the three pillars.
Quality TypeScript, Biome, ESLint Types + linting + formatting, ideally in one fast tool.
AI Claude, GitHub Copilot, Cursor The 2026 productivity multiplier.
Deploy Docker, Fly.io, Railway, Kubernetes Docker everywhere. Fly/Railway for small teams. K8s for scale.

The Habits That Compound

📖

Read other people's code

15 minutes a day reading production code from open-source projects. Fastest way to level up.

✍️

Write one test per bug fix

The bug that never returns is worth more than the code that fixes it once.

🔍

Profile before optimizing

Your assumptions about where time goes are almost always wrong. Measure first.

🎯

Small, frequent deploys

The easiest way to make deploys safe is to make them boring. Ship daily.

🤖

Pair with AI daily

Not to write your code — to review it, test it, and challenge your assumptions.

📝

Write about what you learn

Blog posts, PR descriptions, internal docs. Writing forces clarity.

🎓

Teach someone junior

Explaining something forces you to actually understand it. Mentor if you can.

🌱

Learn one new thing per week

A new library, a new pattern, a new debugging tool. Small, consistent growth beats intense bursts.

10 · Where to Go Next

JavaScript in 2026 is not the JavaScript of 2016 — and it will look different again in 2030. Here are the frontiers worth watching.

Bun

A drop-in Node.js replacement that's 3-5× faster. Includes a built-in bundler, test runner, and package manager. Watch this space.

🦕

Deno

Ryan Dahl's Node.js successor. Security-first, TypeScript-native, web-standard aligned. Deno 2 is production-ready.

🌐

Edge Runtime

V8 isolates on CDN edge nodes. Deploy JavaScript to 300+ locations. Cloudflare Workers, Vercel Edge, Deno Deploy.

🔧

WebAssembly

Run Rust, Go, or C++ in the browser at near-native speed. Increasingly common for compute-heavy JS workloads.

🤖

AI-First Frameworks

Next.js, SvelteKit, and others are adding first-class streaming AI primitives. The line between web app and AI app is blurring.

🧠

Server Components

React Server Components and their equivalents in other frameworks. The biggest architectural shift since SPA.

The Three Career Paths from Here

Path What You'll Do Next Steps
Full-Stack Product Engineer Build complete features end-to-end. Own the database, the API, the UI, and the deploy. Learn a frontend framework deeply. Practice system design. Ship a side project.
Backend / Platform Engineer Own the services, databases, queues, and infrastructure that everything else runs on. Deep-dive Kubernetes, distributed systems, and database internals. Learn Go or Rust.
AI / ML Engineer Build the systems around AI models — orchestration, evaluation, deployment, cost control. Learn prompt engineering, RAG, vector databases, and evaluation frameworks.
🎯

The honest truth about careers: the specific technologies change every 3-5 years. What doesn't change is the ability to learn deeply, debug systematically, write clearly, and ship reliably. The series you just finished didn't just teach you JavaScript — it taught you how to think about any system. That's the transferable skill.

11 · Final Quiz — 15 Questions Across the Entire Series

The final quiz covers material from all seven parts. Score 12 or higher and you've genuinely mastered the material. Score 15 and you're ready to teach it.

🏆

Part 7 — Final Series Quiz

15 questions. Every answer spans all seven parts.
Score: 0 / 15

12 · Cheat Sheet & The End of the Series

The Complete Series Cheat Sheet

Part The One Thing to Remember
Part 1 — Execution LifecycleSync → microtasks → one macrotask. Every async question resolves by asking "where does this fit in that order?"
Part 2 — Async PatternsPromise.all for parallel, await for serial. Batch reads before writes. Abort everything you can.
Part 3 — Object ModelPrototypes are the reality; classes are sugar. this is determined at call site. Closures capture references, not values.
Part 4 — Modern Syntax?? over ||. ESM by default. Generators for streaming. WeakMap for metadata.
Part 5 — Browser RuntimeBatch reads and writes. Delegate events. Animate transform and opacity. Clean up listeners.
Part 6 — Testing & TypesOne test per bug fix. Types at the boundaries. AI writes drafts — you own the final code.
Part 7 — ProductionObservability, deployment safety, security, and the discipline to ship. The code is the easy part.

The Do / Don't for the Entire Career

✅ DO

  • Write code for the next person who reads it — often future-you.
  • Test the fixes, not just the features.
  • Profile before optimizing.
  • Deploy small, deploy often, deploy safely.
  • Log structured events with request IDs.
  • Check authorization, not just authentication.
  • Read error messages carefully — they usually tell you exactly what's wrong.
  • Understand the code you ship. Every line.
  • Teach what you learn. You'll understand it better.
  • Stay curious. Every 3-5 years, the "best" tools change.

❌ DON'T

  • Don't ship code you don't understand — even if AI wrote it.
  • Don't trust coverage numbers as a proxy for correctness.
  • Don't skip observability to ship faster. You'll pay it back with interest at 2 AM.
  • Don't use || for defaults when 0 or '' are valid.
  • Don't deploy on Friday afternoons without a rollback plan.
  • Don't treat security as "someone else's job".
  • Don't argue about tabs vs spaces. Automate the decision.
  • Don't be the only person who understands your service.
  • Don't stop learning when you get comfortable.
  • Don't forget why you started.
🎓 🚀 ✨ 🏆

You Finished the Series

Seven parts. Dozens of chapters. Hundreds of concepts. Three complete capstone projects. When you started, JavaScript might have felt like a stranger — chaotic, unpredictable, a language that "just worked sometimes".

Now you understand it. Not because you memorized syntax — but because you understand the machine underneath. The event loop. The prototype chain. The rendering pipeline. The production concerns. The patterns that scale.

You can now read any JavaScript codebase with confidence. You can ship production services with tests, observability, and safety nets. You can pair with AI without losing control. You can architect systems, not just write functions.

That's not just "knowing JavaScript". That's being a senior engineer who happens to work in JavaScript. And it's exactly what you set out to become.

Now go build something worth shipping. 🚀


Part 7 of 7 · JavaScript for Backend Developers · The End · FreeLearning365.com

🌟 Continue Learning on FreeLearning365

Free tools, tutorials, and question banks for developers, students, and professionals.

🌍 FreeLearning365.com — Your gateway to free learning, tools & resources.

JavaScript for Backend Developers — The Complete 7-Part Series · © 2026 FreeLearning365

Thank you for reading. Now go build something amazing.

🚀 JavaScript for Backend Developers
🗺️ The Complete 7-Part Journey
Navigate anywhere in the series
Learning Path Part 1 of 7
🏆 Part 7 → AI + Observability + Production + 3 Capstone Projects

Post a Comment

0 Comments