Module: MongoDB
MongoDB·148·7 MIN READ

148: Node.js + MongoDB Production Capstone — Multi-Tenant API, Query Plans, Transactions, Search, Tests, Security, and Recovery

TOPICS COVERED: Node.js + MongoDB Production Capstone — Multi-Tenant API, Query Plans, Transactions, Search, Tests, Security, and Recovery

Capstone purpose

This is the final integration project for the Node.js and MongoDB modules.

The goal is not to demonstrate that you can call Express routes or Mongoose methods.

You must prove that you understand:

text
runtime
HTTP
security
state boundaries
Mongo data modeling
indexes
consistency
operations
failure recovery

through one complete production-oriented system.

Project

Build a Multi-Tenant Work Management API.

The system supports:

  • organizations/tenants;
  • users and permissions;
  • projects;
  • tasks;
  • comments;
  • activity events;
  • file metadata;
  • background exports;
  • search;
  • audit history.

You may use:

Persistence option A

Official MongoDB Node.js driver.

Persistence option B

Mongoose 9.

If using Mongoose, you must still demonstrate Mongo indexes, aggregation, transactions, explain plans, and driver/database semantics.

Required technology baseline

text
supported Node LTS
ES modules
Express 5
MongoDB supported 8.x/8.3 environment
mongodb driver modern 7.x line
or Mongoose 9
node:test or approved project test runner

Do not use deprecated callback APIs.

Architecture diagram

text
React/client
   ↓ HTTPS
reverse proxy/load balancer
   ↓
Node process
   ├─ Express
   │  ├─ request ID
   │  ├─ authentication
   │  ├─ validation
   │  ├─ authorization
   │  ├─ routes
   │  └─ error boundary
   │
   ├─ application services
   │
   ├─ repositories
   │  ↓
   │ MongoDB
   │
   └─ background workers
      ├─ outbox publisher
      └─ export worker

Required state boundaries

text
HTTP request state
→ Express/request context

authenticated user/tenant
→ verified server auth context

business rules
→ service/domain

Mongo query construction
→ repository

durable truth
→ MongoDB

background event delivery
→ outbox/job infrastructure

logs/traces
→ observability

Do not use global mutable “currentTenant.”

Collections

Minimum:

text
users
memberships
projects
tasks
comments
audit_events
outbox
idempotency_keys
export_jobs

Optional:

text
files
search_documents
notifications

Tenant model

Every tenant-owned collection includes:

javascript
tenantId: ObjectId

Example task:

javascript
{
  _id: ObjectId(...),
  tenantId: ObjectId(...),
  projectId: ObjectId(...),
  title: "Prepare report",
  description: "...",
  status: "open",
  priority: "high",
  assigneeId: ObjectId(...),
  tags: [
    "finance",
    "monthly"
  ],
  version: 4,
  createdBy: ObjectId(...),
  createdAt: Date,
  updatedAt: Date
}

Membership

javascript
{
  tenantId,
  userId,
  role: "manager",
  permissions: [
    "project:read",
    "task:create",
    "task:update"
  ]
}

Unique:

javascript
{
  tenantId: 1,
  userId: 1
}

Do not accept tenantId from login request as authority.

Authenticated session chooses authorized tenant membership.

Project model

javascript
{
  _id,
  tenantId,
  name,
  status,
  ownerId,
  createdAt,
  updatedAt
}

Index:

javascript
{
  tenantId: 1,
  status: 1,
  updatedAt: -1,
  _id: -1
}

if list access matches.

Task indexes

Required query:

text
tenant
project
status
sort updated desc
cursor

Candidate:

javascript
{
  tenantId: 1,
  projectId: 1,
  status: 1,
  updatedAt: -1,
  _id: -1
}

Another:

text
assigned to user + open tasks

Candidate:

javascript
{
  tenantId: 1,
  assigneeId: 1,
  status: 1,
  updatedAt: -1
}

Do not create both automatically without validating actual workloads.

You must provide explain("executionStats") evidence.

API endpoints

Authentication/session

text
POST   /auth/login
POST   /auth/logout
GET    /me

You may integrate an external identity provider instead of implementing password login, but document trust boundary.

Projects

text
GET    /projects
POST   /projects
GET    /projects/:projectId
PATCH  /projects/:projectId

Tasks

text
GET    /projects/:projectId/tasks
POST   /projects/:projectId/tasks
GET    /tasks/:taskId
PATCH  /tasks/:taskId
DELETE /tasks/:taskId

Comments

text
GET    /tasks/:taskId/comments
POST   /tasks/:taskId/comments
text
GET /search?q=...&projectId=...

Exports

text
POST /exports
GET  /exports/:exportId

Request validation

Every route validates:

text
params
query
body
headers where needed

Use one schema library consistently.

Reject or strip unknown fields intentionally.

You must demonstrate defense against:

json
{
  "tenantId": "anotherTenant",
  "role": "admin",
  "$where": "..."
}

None may become persistence/query authority.

Authentication

Session or token strategy must include:

  • verification;
  • expiry;
  • logout/revocation behavior;
  • secure storage;
  • rate limiting;
  • audit.

If password:

  • modern password hashing;
  • no plaintext;
  • login rate limiting.

Authorization

At minimum:

text
viewer
member
manager
tenant-admin

But route checks should operate on permissions/resource attributes.

Example:

text
task:update
+
same tenant
+
project active
+
user assignee/manager policy

Do not implement only:

js
if (role === 'admin')

everywhere.

Cross-tenant test requirement

For every resource family, include tests:

text
tenant A cannot read tenant B
tenant A cannot update tenant B
tenant A cannot infer resource existence

This is a release blocker.

Project creation

Use server-created:

text
tenantId
ownerId
timestamps
version

Do not accept them raw from body.

Task optimistic concurrency

PATCH includes:

json
{
  "version": 4,
  "title": "Updated"
}

Filter:

javascript
{
  _id: taskId,
  tenantId,
  version: 4
}

Update:

javascript
{
  $set: {
    title: "...",
    updatedAt: new Date()
  },
  $inc: {
    version: 1
  }
}

Stale:

text
409 VERSION_CONFLICT

Frontend can refetch/reconcile.

Task delete

Define:

  • hard delete;
  • soft delete;
  • archive.

If soft delete:

javascript
deletedAt

Then adjust:

  • every read query;
  • unique indexes;
  • search;
  • comments;
  • audit;
  • retention.

Do not add soft delete casually.

Comments modeling

Task may have unbounded comments.

Do not embed all forever in task document.

Separate:

javascript
{
  _id,
  tenantId,
  taskId,
  authorId,
  body,
  createdAt
}

Index:

javascript
{
  tenantId: 1,
  taskId: 1,
  createdAt: 1,
  _id: 1
}

Cursor pagination.

Audit events

javascript
{
  _id,
  tenantId,
  actorId,
  action: "task.statusChanged",
  resourceType: "task",
  resourceId: taskId,
  before: {...small safe fields},
  after: {...small safe fields},
  requestId,
  occurredAt
}

Do not store secrets/full sensitive request body.

Audit records should be append-oriented.

Transaction workflow

Required workflow:

Completing a project must mark project completed, create an audit record, and create an outbox event atomically.

Transaction:

text
project update
audit insert
outbox insert
commit

No external webhook inside transaction.

Outbox publishes:

text
project.completed

after commit.

Outbox unique/idempotency

Each event has stable ID.

Consumer uses event ID to avoid duplicate effects.

Publisher retry may deliver more than once.

Design for at-least-once delivery.

External webhook

If capstone sends webhook:

  • HMAC signature;
  • timestamp;
  • event ID;
  • retries with backoff;
  • timeout;
  • max attempts;
  • dead-letter/manual review;
  • no transaction-held network call.

Export job

POST:

text
/exports

returns:

text
202 Accepted
json
{
  "data": {
    "exportId": "..."
  }
}

Worker streams tasks/comments to NDJSON or CSV.

Do not load all documents with toArray().

Use cursor + stream/file/object storage.

Export job durability

Store in Mongo:

javascript
{
  _id,
  tenantId,
  requestedBy,
  status: "pending",
  createdAt,
  startedAt,
  completedAt,
  failedAt,
  leaseUntil,
  resultLocation,
  attempts
}

Workers claim atomically.

On crash, lease expires/reclaim.

Search implementation choices

Baseline

MongoDB Search where available.

Search:

text
title
description
comments maybe

Tenant/project filter included.

Alternative

Classic indexed search only if feature requirements are simple.

Optional vector extension

Semantic search with embeddings.

Must filter tenant/project inside retrieval.

Do not rely on post-filter.

Search authority

Search result finds candidate task IDs.

Before performing sensitive write, ordinary tenant-scoped database query still verifies resource current state/authorization.

Search index is not transaction authority.

Aggregation requirement

Create dashboard:

text
open tasks by priority
completed this week
tasks by assignee
average completion time

Pipeline:

  1. tenant match first;
  2. bounded date/project scope;
  3. group;
  4. project.

Provide explain/performance evidence.

If exact large dashboard is expensive, implement materialized summary.

Index evidence requirement

For each critical endpoint, provide:

text
query pattern
index chosen
explain before
explain after
nReturned
totalDocsExamined
totalKeysExamined
sort stage

A screenshot is not enough; keep machine-readable notes in project docs.

Slow-query budget

Example:

text
task list p95 < 150 ms at database layer under test dataset

Your actual target can differ.

Load realistic dataset.

Do not benchmark 20 documents then claim scalable.

Seed volume

Suggested performance dataset:

text
100 tenants
1,000 projects
1,000,000 tasks
5,000,000 comments

You may scale according to hardware, but dataset must expose index/pagination behavior.

Use generated non-sensitive data.

Cursor pagination requirement

No deep skip for high-volume task/comment endpoints.

Cursor must be:

  • opaque;
  • validated;
  • stable;
  • tied to sort.

Test insertion between pages.

Ensure no duplicate/missing result beyond expected concurrent-data semantics.

Rate limits

At minimum:

text
login
search
exports
webhook resend/admin

Shared/distributed limiter when multiple app instances.

Do not keep only per-process Map in scalable production design.

Body limits

Separate:

text
JSON normal API 256 KB
comment 32 KB logical max
file upload separate streaming limit

Reverse proxy and Express limits aligned.

File handling

If files included:

  • object storage preferred baseline;
  • Mongo stores metadata;
  • signed URL/access endpoint;
  • tenant authorization;
  • content type/size/hash;
  • malware status.

GridFS allowed if justified and streamed.

Error contract

Required public codes:

text
VALIDATION_ERROR
UNAUTHENTICATED
FORBIDDEN
NOT_FOUND
CONFLICT
VERSION_CONFLICT
RATE_LIMITED
BODY_TOO_LARGE
DATABASE_UNAVAILABLE
UPSTREAM_UNAVAILABLE
INTERNAL_ERROR

Do not expose Mongo stack/index names/URI.

HTTP semantics

Examples:

text
201 create
204 delete no body
401 missing/invalid auth
403 known forbidden
404 hidden/unavailable resource policy
409 version/business conflict
413 oversized
415 unsupported media
422 validation
429 rate limit
503 dependency unavailable

Use consistently.

Request correlation

Every request:

text
requestId

log:

text
route template
status
duration
error code
release

Do not use raw URL with secrets.

Metrics

Track:

text
request rate
latency p50/p95/p99
5xx
Mongo query latency
pool checkout wait
event-loop delay
RSS/heap
outbox backlog
export queue depth
webhook retry count
replication lag if available

Avoid tenant/user IDs as metrics labels.

Tracing

Span:

text
PATCH /tasks/:id
→ auth
→ Mongo update
→ outbox transaction

Use OpenTelemetry/APM if available.

Sanitize DB query attributes.

Health

text
/live
/ready

Readiness false during shutdown.

Brief Mongo election should not necessarily kill liveness.

Graceful shutdown requirement

SIGTERM test:

text
readiness false
server stops accepting
workers stop claiming
active transaction finishes/aborts
cursors/change streams close
MongoClient closes
process exits before deadline

Automate process test.

Backup requirement

Create documented backup strategy:

  • Atlas snapshot/PITR or supported equivalent;
  • RPO;
  • RTO;
  • retention;
  • encryption;
  • access.

Then perform a restore test into isolated environment.

Record actual restore time.

A plan without restore evidence is incomplete.

Schema migration requirement

Introduce one schema evolution.

Example:

v1:

javascript
{
  priority: "high"
}

v2:

javascript
{
  priority: {
    code: "high",
    rank: 3
  }
}

Or another realistic change.

Plan:

  • compatible reads;
  • migration;
  • validator transition;
  • index change;
  • rollback.

Do not rewrite all documents live with no rollout strategy.

Index migration requirement

Add a new compound index using controlled deployment.

Demonstrate:

  • build monitoring;
  • app compatibility before/after;
  • old index removal decision;
  • hidden index if useful;
  • rollback.

Failure injection matrix

You must deliberately simulate:

Node/runtime

text
uncaught unexpected error
event-loop CPU blocking
SIGTERM
worker crash

Mongo

text
database unavailable
primary failover
duplicate key
slow query
transaction retry/conflict

Network

text
upstream timeout
webhook timeout
client disconnect

Input/security

text
oversized body
malformed JSON
NoSQL operator-shaped input
cross-tenant ObjectId
expired session
rate limit

For each write:

text
user-visible result
HTTP status
log
metric
retry
cleanup
data correctness

Testing requirements

Unit

  • validation;
  • cursor encoding;
  • policy/RBAC/ABAC;
  • domain transitions;
  • error mapping.

Mongo integration

  • indexes;
  • unique constraint;
  • repository tenant scope;
  • transaction;
  • optimistic concurrency;
  • aggregation;
  • cursor pagination;
  • outbox claiming.

HTTP integration

  • Express middleware;
  • auth;
  • validation;
  • status/error contract;
  • rate limits;
  • body limit.

Process

  • invalid config startup;
  • graceful SIGTERM;
  • worker failure.

E2E

At least:

text
login
create project
create task
edit
conflict
search
complete project
observe audit/event

If React frontend exists, use Playwright.

Security tests

Mandatory:

text
cross-tenant read
cross-tenant write
NoSQL injection-shaped body/query
mass assignment
token/session invalid
CSRF if cookie auth
CORS policy
rate limit
path/file authorization
webhook signature/replay

Performance tests

Measure before/after:

  • index;
  • cursor pagination;
  • lean if Mongoose;
  • aggregation;
  • worker export.

Do not optimize using anecdotal “feels faster.”

Architecture review document

Include:

Data ownership

text
MongoDB = durable truth
Search index = retrieval projection
outbox = pending durable integration events
in-memory state = only ephemeral process state

Trust boundaries

text
browser
HTTP
auth
service
Mongo
external webhook
object storage

Failure boundaries

text
validation
auth
database
external
worker
process

Consistency boundaries

text
single document atomic
Mongo transaction
eventual outbox publish
search index lag

Final review questions

You must be able to answer:

  1. Why is Node not Express?
  2. Why does the API create one MongoClient per process?
  3. Why is MongoClient pool size not “as large as possible”?
  4. Why is ObjectId not an authorization mechanism?
  5. Why does tenant scope belong in the Mongo query?
  6. Why is unique: true not an application validation check?
  7. When is lean() appropriate?
  8. Why can transaction callbacks not call non-idempotent external services?
  9. Why does cursor pagination require a matching deterministic sort/index?
  10. Why can a change stream deliver duplicate effects after recovery?
  11. Why is replication not backup?
  12. Why can a search index not be authoritative for a payment/permission check?
  13. When should data be embedded instead of referenced?
  14. What does explain() prove?
  15. What should happen during primary election?
  16. How does graceful shutdown protect deploys?
  17. What are RPO and RTO?
  18. Why is raw req.query unsafe as a Mongo filter?
  19. How would you recover after a bad migration?
  20. Which metrics show database versus event-loop bottlenecks?

Completion standard

This capstone is complete only when:

text
[ ] architecture is documented
[ ] all inputs validated
[ ] tenant security tested
[ ] indexes exist and are explained
[ ] cursor pagination used for high-volume lists
[ ] concurrency conflict implemented
[ ] one real transaction implemented
[ ] external side effects use outbox/idempotency
[ ] graceful shutdown tested
[ ] integration tests use real Mongo topology where needed
[ ] performance evidence exists
[ ] backup restore tested
[ ] schema/index migration demonstrated
[ ] failure injection completed
[ ] secrets/logging reviewed
[ ] deployment runbook written

At that point the learner has not merely used MongoDB—they can reason about a Node + MongoDB production system.

Official references