148: 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:
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
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
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
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:
users memberships projects tasks comments audit_events outbox idempotency_keys export_jobs
Optional:
files search_documents notifications
Tenant model
Every tenant-owned collection includes:
tenantId: ObjectId
Example task:
{
_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
{
tenantId,
userId,
role: "manager",
permissions: [
"project:read",
"task:create",
"task:update"
]
}
Unique:
{
tenantId: 1,
userId: 1
}
Do not accept tenantId from login request as authority.
Authenticated session chooses authorized tenant membership.
Project model
{ _id, tenantId, name, status, ownerId, createdAt, updatedAt }
Index:
{
tenantId: 1,
status: 1,
updatedAt: -1,
_id: -1
}
if list access matches.
Task indexes
Required query:
tenant project status sort updated desc cursor
Candidate:
{
tenantId: 1,
projectId: 1,
status: 1,
updatedAt: -1,
_id: -1
}
Another:
assigned to user + open tasks
Candidate:
{
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
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
GET /projects POST /projects GET /projects/:projectId PATCH /projects/:projectId
Tasks
GET /projects/:projectId/tasks POST /projects/:projectId/tasks GET /tasks/:taskId PATCH /tasks/:taskId DELETE /tasks/:taskId
Comments
GET /tasks/:taskId/comments POST /tasks/:taskId/comments
Search
GET /search?q=...&projectId=...
Exports
POST /exports GET /exports/:exportId
Request validation
Every route validates:
params query body headers where needed
Use one schema library consistently.
Reject or strip unknown fields intentionally.
You must demonstrate defense against:
{
"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:
viewer member manager tenant-admin
But route checks should operate on permissions/resource attributes.
Example:
task:update + same tenant + project active + user assignee/manager policy
Do not implement only:
if (role === 'admin')
everywhere.
Cross-tenant test requirement
For every resource family, include tests:
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:
tenantId ownerId timestamps version
Do not accept them raw from body.
Task optimistic concurrency
PATCH includes:
{
"version": 4,
"title": "Updated"
}
Filter:
{
_id: taskId,
tenantId,
version: 4
}
Update:
{
$set: {
title: "...",
updatedAt: new Date()
},
$inc: {
version: 1
}
}
Stale:
409 VERSION_CONFLICT
Frontend can refetch/reconcile.
Task delete
Define:
- hard delete;
- soft delete;
- archive.
If soft delete:
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:
{ _id, tenantId, taskId, authorId, body, createdAt }
Index:
{
tenantId: 1,
taskId: 1,
createdAt: 1,
_id: 1
}
Cursor pagination.
Audit events
{
_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:
project update audit insert outbox insert commit
No external webhook inside transaction.
Outbox publishes:
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:
/exports
returns:
202 Accepted
{
"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:
{
_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:
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:
open tasks by priority completed this week tasks by assignee average completion time
Pipeline:
- tenant match first;
- bounded date/project scope;
- group;
- project.
Provide explain/performance evidence.
If exact large dashboard is expensive, implement materialized summary.
Index evidence requirement
For each critical endpoint, provide:
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:
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:
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:
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:
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:
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:
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:
requestId
log:
route template status duration error code release
Do not use raw URL with secrets.
Metrics
Track:
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:
PATCH /tasks/:id → auth → Mongo update → outbox transaction
Use OpenTelemetry/APM if available.
Sanitize DB query attributes.
Health
/live /ready
Readiness false during shutdown.
Brief Mongo election should not necessarily kill liveness.
Graceful shutdown requirement
SIGTERM test:
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:
{
priority: "high"
}
v2:
{
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
uncaught unexpected error event-loop CPU blocking SIGTERM worker crash
Mongo
database unavailable primary failover duplicate key slow query transaction retry/conflict
Network
upstream timeout webhook timeout client disconnect
Input/security
oversized body malformed JSON NoSQL operator-shaped input cross-tenant ObjectId expired session rate limit
For each write:
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:
login create project create task edit conflict search complete project observe audit/event
If React frontend exists, use Playwright.
Security tests
Mandatory:
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
MongoDB = durable truth Search index = retrieval projection outbox = pending durable integration events in-memory state = only ephemeral process state
Trust boundaries
browser HTTP auth service Mongo external webhook object storage
Failure boundaries
validation auth database external worker process
Consistency boundaries
single document atomic Mongo transaction eventual outbox publish search index lag
Final review questions
You must be able to answer:
- Why is Node not Express?
- Why does the API create one MongoClient per process?
- Why is MongoClient pool size not “as large as possible”?
- Why is
ObjectIdnot an authorization mechanism? - Why does tenant scope belong in the Mongo query?
- Why is
unique: truenot an application validation check? - When is
lean()appropriate? - Why can transaction callbacks not call non-idempotent external services?
- Why does cursor pagination require a matching deterministic sort/index?
- Why can a change stream deliver duplicate effects after recovery?
- Why is replication not backup?
- Why can a search index not be authoritative for a payment/permission check?
- When should data be embedded instead of referenced?
- What does
explain()prove? - What should happen during primary election?
- How does graceful shutdown protect deploys?
- What are RPO and RTO?
- Why is raw
req.queryunsafe as a Mongo filter? - How would you recover after a bad migration?
- Which metrics show database versus event-loop bottlenecks?
Completion standard
This capstone is complete only when:
[ ] 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
- https://nodejs.org/api/
- https://expressjs.com/
- https://www.mongodb.com/docs/manual/
- https://www.mongodb.com/docs/drivers/node/current/
- https://mongoosejs.com/docs/
- https://www.mongodb.com/docs/manual/indexes/
- https://www.mongodb.com/docs/manual/aggregation/
- https://www.mongodb.com/docs/manual/core/transactions/
- https://www.mongodb.com/docs/manual/replication/
- https://www.mongodb.com/docs/manual/sharding/
- https://www.mongodb.com/docs/manual/security/
- https://www.mongodb.com/docs/manual/core/backups/
- https://roadmap.sh/nodejs
- https://roadmap.sh/mongodb
