Module: Nodejs
Nodejs·133·4 MIN READ

133: Node.js Production Capstone — CLI, HTTP API, Express, Workers, Testing, Security, and Operations

TOPICS COVERED: Node.js Production Capstone — CLI, HTTP API, Express, Workers, Testing, Security, and Operations

Learning objectives

This capstone verifies that you understand Node as a runtime, not only Express syntax.

You will build a production-oriented Task Processing Service with:

  • CLI;
  • Express HTTP API;
  • configuration;
  • validation;
  • authentication/authorization boundary;
  • streams;
  • worker thread job;
  • outbound fetch;
  • graceful shutdown;
  • tests;
  • observability;
  • security controls.

MongoDB is intentionally not yet required. Persistence can use an in-memory repository or filesystem for the capstone baseline. The next module replaces repository with MongoDB.

Architecture

text
CLI / HTTP
    ↓
input validation
    ↓
application services
    ↓
repository abstraction
    ↓
in-memory/file implementation

HTTP service
├─ auth middleware
├─ request context
├─ task routes
├─ report route
└─ error boundary

background
└─ worker pool for CPU report calculation

external
└─ fetch upstream metadata API

Required file structure

text
src/
├─ app/
│  ├─ create-app.js
│  └─ lifecycle.js
├─ config/
│  └─ config.js
├─ domain/
│  ├─ errors.js
│  ├─ task-service.js
│  └─ task-policy.js
├─ http/
│  ├─ middleware/
│  │  ├─ auth.js
│  │  ├─ request-id.js
│  │  └─ error-handler.js
│  └─ routes/
│     ├─ tasks.js
│     └─ reports.js
├─ repositories/
│  └─ memory-task-repository.js
├─ workers/
│  ├─ report-worker.js
│  └─ report-pool.js
├─ clients/
│  └─ metadata-client.js
├─ cli/
│  └─ task-cli.js
└─ server.js

The exact names can differ, but ownership must be equally clear.

Configuration

Validate at startup:

text
PORT
APP_ENV
UPSTREAM_URL
REQUEST_BODY_LIMIT
REPORT_WORKERS

Secret values should not be printed.

Example:

js
function readInteger(name, fallback, { min, max }) {
  const raw = process.env[name];

  if (raw === undefined) {
    return fallback;
  }

  const value = Number(raw);

  if (
    !Number.isInteger(value) ||
    value < min ||
    value > max
  ) {
    throw new Error(`Invalid ${name}`);
  }

  return value;
}

Repository contract

js
export function createMemoryTaskRepository() {
  const records = new Map();

  return {
    async insert(task) {
      ...
    },

    async findById({ taskId, tenantId }) {
      ...
    },

    async list({ tenantId, cursor, limit }) {
      ...
    },

    async update({ taskId, tenantId, patch, version }) {
      ...
    },

    async delete({ taskId, tenantId }) {
      ...
    },
  };
}

Every lookup is tenant-scoped.

MongoDB implementation later must preserve contract.

Authentication stub

For capstone, you can use signed development API keys or a controlled auth adapter rather than implementing full OAuth.

Example server context:

js
{
  userId: 'u1',
  tenantId: 'tenant-a',
  permissions: new Set([
    'task:read',
    'task:create',
    'task:update',
  ]),
}

Tests must include cross-tenant denial.

Task API contract

text
GET    /tasks
POST   /tasks
GET    /tasks/:taskId
PATCH  /tasks/:taskId
DELETE /tasks/:taskId
POST   /reports
GET    /health/live
GET    /health/ready

Create task

Request:

json
{
  "title": "Generate monthly report",
  "priority": "high"
}

Response 201:

json
{
  "data": {
    "task": {
      "id": "t1",
      "title": "Generate monthly report",
      "priority": "high",
      "completed": false,
      "version": 1
    }
  }
}

Validate unknown fields.

PATCH with concurrency

json
{
  "version": 1,
  "title": "Updated report"
}

If stored version is 2:

text
409 Conflict

Response:

json
{
  "error": {
    "code": "VERSION_CONFLICT",
    "message": "The task changed. Reload and try again."
  }
}

Pagination

Use cursor:

text
GET /tasks?limit=25&after=...

Stable order:

text
createdAt DESC, id DESC

Even in memory, design cursor so Mongo migration can preserve semantics.

Request limits

JSON:

text
256 KB maximum

Report upload/stream endpoints get separate limits.

Test 413.

Report worker

POST /reports accepts bounded dataset/parameters.

CPU-heavy summary calculation runs worker thread.

Do not block main server.

For long report, return:

text
202 Accepted

with job ID.

For course scope, worker pool can keep jobs in memory; document that this is not crash-durable.

Later architecture can use external queue.

Worker pool rules

  • fixed bounded worker count;
  • max queue length;
  • reject 503 when queue saturated;
  • worker crash replacement;
  • shutdown waits/cancels;
  • metrics for queue depth/duration.

Upstream fetch

Metadata client:

js
export async function getCategoryMetadata(
  category,
  {
    signal,
  } = {},
) {
  const url = new URL(
    `/categories/${encodeURIComponent(category)}`,
    config.upstreamUrl,
  );

  const response = await fetch(url, {
    signal,
  });

  if (!response.ok) {
    throw new UpstreamError(...);
  }

  return response.json();
}

Use fixed trusted base URL.

Do not accept arbitrary URL from client.

Timeout

Combine request/shutdown/upstream timeout signals where supported.

Every upstream request has deadline.

Test timeout.

Request context

Use request ID:

text
response X-Request-Id
structured logs
upstream trace header if safe

AsyncLocalStorage can make it available in deep logging.

Business functions should still receive explicit authorization inputs.

Logging

Example:

json
{
  "level": "info",
  "requestId": "r123",
  "method": "POST",
  "route": "/tasks",
  "status": 201,
  "durationMs": 18
}

Never log auth secret/body blindly.

Error mapping

Required public codes:

text
VALIDATION_ERROR
UNAUTHENTICATED
FORBIDDEN
NOT_FOUND
VERSION_CONFLICT
BODY_TOO_LARGE
UPSTREAM_UNAVAILABLE
REPORT_QUEUE_FULL
INTERNAL_ERROR

Unexpected internal error gets 500 safe message.

Graceful shutdown

SIGTERM:

  1. readiness false;
  2. stop accepting;
  3. abort pollers/upstream work;
  4. stop worker queue accepting;
  5. wait bounded in-flight;
  6. close workers;
  7. close server;
  8. exit.

Test in child process.

CLI

Commands:

text
task-cli list
task-cli add "title"
task-cli complete <id>
task-cli export --output file.ndjson

CLI should call same service/repository contract where local mode, or HTTP API where remote mode depending your design.

Do not duplicate business validation.

Streaming export

task-cli export should stream NDJSON:

json
{"id":"t1","title":"A"}
{"id":"t2","title":"B"}

Use stream pipeline to file/stdout.

No full array buffering for large export.

Test suite

Unit

  • title normalization;
  • authorization policy;
  • cursor encoding/decoding;
  • version conflict;
  • worker calculation.

HTTP integration

  • auth;
  • validation;
  • body limit;
  • CRUD;
  • pagination;
  • conflict;
  • 404;
  • safe 500;
  • queue saturation.

Process

  • startup invalid config;
  • SIGTERM shutdown;
  • CLI exit codes.

Security

  • cross-tenant ID;
  • unknown admin field;
  • wrong content type;
  • command/path injection where relevant;
  • secret not logged.

Load/performance test

Measure:

  • baseline request p50/p95/p99;
  • CPU endpoint before worker;
  • after worker;
  • event-loop delay;
  • worker queue saturation.

Document evidence.

Failure injection

Deliberately simulate:

text
upstream timeout
worker crash
queue full
repository failure
client disconnect
SIGTERM during report
malformed JSON
oversized JSON
cross-tenant request

For each, write:

text
HTTP/CLI result
log result
cleanup result
retry behavior

Deployment document

Write:

text
Node version
install command
start command
required env vars
health endpoints
SIGTERM grace period
CPU/memory assumptions
worker count
log format
known non-durable in-memory state

Review questions

  1. Which code is Node-specific versus Express-specific?
  2. Why is API contract independent of Express?
  3. Why is CPU report not fixed by async?
  4. Why is worker pool bounded?
  5. Why are task queries tenant-scoped?
  6. Why is CORS not authentication?
  7. Why does upstream fetch need timeout?
  8. Why is body size security?
  9. Why do tests use port 0?
  10. What would MongoDB replace without changing HTTP contract?

Exit criteria

You are ready for MongoDB only if you can explain:

text
JavaScript runtime
modules/packages
process/config
event loop
errors
filesystem
buffers/streams
events
HTTP/fetch
API contracts
Express
security
authentication
workers/processes
testing
debugging
production lifecycle

without describing everything as “Express handles it.”

Official references