FullStack Course LogoFullStack Course
Module: MongoDB
MongoDB·148·18 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 in isolation. The capstone is a test of whether you can connect the runtime, HTTP layer, security model, data model, and operational behavior into one system whose boundaries are explicit.

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. A feature that works on the happy path but leaks another tenant's data, loses an event, or cannot be recovered is not complete.

Project

Build a Multi-Tenant Work Management API.

Treat this as a production design and implementation exercise, not just a CRUD exercise. The API should make tenant scope, authorization, consistency, and failure behavior visible in its code and its evidence.

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.

An ODM can change the application API, but it does not remove the database decisions underneath it. You are still responsible for knowing which query reaches MongoDB, which index it uses, and what consistency guarantees the operation has.

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.

Use promise-based APIs throughout the application, and make connection, shutdown, and error behavior explicit rather than relying on implicit driver defaults.

Architecture diagram

The diagram separates request handling from business logic, persistence, and background work. Keep those boundaries real in the implementation: routes should not become a second repository, and workers should not bypass the same tenant and authorization rules used by the API.

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

The point of these boundaries is to prevent request-specific identity or mutable process state from becoming accidental global state. Pass the authenticated context deliberately, keep query construction in the repository, and make durable state live in MongoDB rather than in a Node process that can be restarted.

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.”

Concurrent requests make such a value unsafe: one request can overwrite it while another request is still running. The tenant must come from the verified request context and be included explicitly in the operation that reaches MongoDB.

Collections

Start with separate collections for entities that grow independently or need their own query and retention policies. The minimum set gives the capstone a durable home for identity, work data, audit history, integration delivery, idempotency, and asynchronous exports.

Minimum:

text
users
memberships
projects
tasks
comments
audit_events
outbox
idempotency_keys
export_jobs

Optional:

text
files
search_documents
notifications

Tenant model

Tenant ownership is a data-invariant, not merely a route convention. Every read and write involving a tenant-owned document must carry the server-derived tenant scope, and tests must prove that an identifier from another tenant is not enough to cross that boundary.

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

Membership connects a user to a tenant and supplies the authorization inputs used by the application. The compound unique constraint prevents duplicate membership records for the same user in the same tenant; it does not, by itself, decide which actions that membership permits.

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

Unique:

javascript
{
  tenantId: 1,
  userId: 1
}

Do not accept tenantId from a login request as authority.

The authenticated session chooses an authorized tenant membership. A client may request a tenant context, but the server must verify that the user is actually a member before constructing the request's authorization context.

Project model

Projects are tenant-owned resources. The application, rather than the caller, supplies ownership and timestamps so that a request cannot create a project in another tenant or forge its history.

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

Index:

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

Use this index when the project list filters by tenant and status and orders by the most recently updated projects. Validate that assumption against the actual list query and workload before treating the index as proven.

Task indexes

Indexes should follow real query shapes, including equality filters and the deterministic sort used for pagination. The candidates below are starting points for the required queries, not a reason to add every possible compound index.

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. Each additional index consumes storage and adds write and maintenance cost.

You must provide explain("executionStats") evidence. The plan should show whether the intended index is selected and whether the examined-document and examined-key counts are reasonable for the dataset.

API endpoints

These endpoints define the minimum externally visible surface. Keep the resource identifiers and status codes consistent across route handlers, services, and tests; a clean route list does not compensate for inconsistent authorization or error behavior underneath it.

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

Validation is the boundary between untrusted HTTP input and typed application intent. Validate each input location separately, then pass only the fields the service is designed to accept.

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 of these fields may become persistence or query authority. In particular, accepting arbitrary objects into a Mongo filter can turn operator-shaped input into behavior the route never intended.

Authentication

Authentication establishes the identity behind a request. It must be possible to explain how credentials are verified, how they expire, how logout prevents reuse where required, and how suspicious attempts are limited and recorded.

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

Authorization is a decision about a specific action on a specific resource. Roles can provide a useful starting vocabulary, but the final check should combine the required permission with tenant membership and the resource's current attributes.

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 authorization only as:

js
if (role === 'admin')

everywhere.

Cross-tenant test requirement

A hidden resource should not be distinguishable through a convenient alternate endpoint, timing-sensitive error, or update response. Repeat these checks for every resource family, including comments, exports, files, audit events, and search results where those features exist.

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. One successful cross-tenant test is not evidence that the whole resource family is safe.

Project creation

Creation is where server-owned fields are easiest to protect because the document does not exist yet. Build these values from the verified request context and server clock, not from a body that the client controls.

Use server-created:

text
tenantId
ownerId
timestamps
version

Do not accept them raw from body.

Task optimistic concurrency

A task can be edited by two requests that both read the same previous version. Optimistic concurrency lets the database accept the first matching update and reject the second without holding a long-lived lock or silently overwriting the first change.

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.

The 409 is an expected concurrency outcome, not an internal server error. The client can refetch the current task, show the conflicting changes, and let the user reconcile them rather than retrying the stale write blindly.

Task delete

Deletion is a product and operations decision, not just a choice between two MongoDB methods. Decide whether records must disappear, remain recoverable, or remain visible as archived before implementing the endpoint.

Define:

  • hard delete;
  • soft delete;
  • archive.

If soft delete:

javascript
deletedAt

Then adjust every behavior that can observe the document:

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

Do not add soft delete casually. It changes filters, uniqueness, storage growth, search results, audit expectations, and retention work across the system.

Comments modeling

Comments are a classic unbounded-child relationship. Keeping every comment inside the task document would make that document grow without a practical bound and would make ordinary task reads increasingly expensive.

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
}

Use cursor pagination. The index's order gives the cursor a stable basis and avoids walking past a large number of skipped comments.

Audit events

An audit event records the security and business history of a change, so it should be useful to an investigator without becoming a second copy of sensitive application data. Keep the before and after values deliberately small and safe.

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. Prefer inserting a new event to rewriting history, and protect the event's tenant and actor fields just as carefully as the resource itself.

Transaction workflow

The transaction covers the durable state that must agree: the project transition, its audit record, and the event waiting to be published. The external side effect happens later, after commit, because a network call cannot be rolled back by MongoDB.

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

Do not make an external webhook call inside the transaction. A transaction callback may be retried, and repeating a non-idempotent network operation could produce duplicate effects or hold database resources during an unreliable call.

Outbox publishes:

text
project.completed

after commit. The publisher can retry the durable outbox record independently of the request that created it.

Outbox unique/idempotency

At-least-once delivery is the practical failure model: a publisher can crash after the consumer receives an event but before the publisher records success. Stable event identity and consumer-side deduplication make that retry safe.

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 rather than assuming exactly-once execution.

External webhook

The webhook boundary is untrusted and failure-prone in both directions. Sign what you send, give the receiver enough information to reject replays, and make retries bounded and observable.

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

Export requests should acknowledge that the work is asynchronous. Returning 202 Accepted means the request was accepted for processing; it does not claim that the file is already available.

POST:

text
/exports

returns:

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

The worker streams tasks and comments to NDJSON or CSV. Streaming keeps memory usage tied to the active batch rather than to the total export size.

Do not load all documents with toArray().

Use a cursor plus a stream and file or object storage. The worker needs a clear way to resume, fail, and publish the resulting location.

Export job durability

The job document is the durable coordination record. Atomic claiming and a lease prevent two workers from processing the same pending job under normal conditions, while lease expiry gives the system a recovery path after a worker crash.

Store in Mongo:

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

Workers claim atomically.

On crash, the lease expires and another worker can reclaim the job according to the retry policy.

Search implementation choices

Search is a retrieval feature, so choose the implementation based on the required matching behavior, scale, and deployment capabilities. Whichever option you choose, tenant and project scope remain part of the security design rather than an afterthought.

Baseline

Use MongoDB Search where it is available and appropriate for the deployment.

Search:

text
title
description
comments maybe

Tenant/project filter included.

Alternative

Use classic indexed search only when the feature requirements are simple enough for ordinary indexes and predicates.

Optional vector extension

Semantic search with embeddings.

Must filter tenant/project inside retrieval.

Do not rely on post-filter.

Search authority

Search data can lag behind the source collection or contain a stale candidate. It is useful for finding IDs, but it cannot authorize a sensitive mutation or stand in for the current durable record.

Search result finds candidate task IDs.

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

The search index is not transaction authority. Re-read the resource through an ordinary tenant-scoped database query before a sensitive write.

Aggregation requirement

The dashboard is intentionally a mix of counts, grouping, and elapsed-time analysis. Start by reducing the pipeline to the authorized tenant and bounded time or project scope; grouping a broad unfiltered collection first is both a security risk and an avoidable performance cost.

Create dashboard:

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

Pipeline:

  1. match the tenant first;
  2. apply a bounded date or project scope;
  3. group the remaining documents;
  4. project only the fields the dashboard needs.

Provide explain and performance evidence. If an exact dashboard over the live data is too expensive at the required scale, make the trade-off explicit and implement a materialized summary instead.

If exact large dashboard is expensive, implement materialized summary.

Index evidence requirement

An index declaration alone does not prove that an endpoint is efficient. Capture evidence for each critical query under a representative dataset so another developer can inspect the plan and reproduce the conclusion.

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 documentation. Record the query shape, plan, and execution statistics rather than relying on a visual claim that the query is fast.

Slow-query budget

A budget turns performance from a preference into a testable constraint. Define where the measurement begins and ends, use realistic data and concurrency, and record the conditions under which the result was obtained.

Example:

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

Your actual target can differ, but document the target and why it fits the application.

Load realistic dataset.

Do not benchmark 20 documents and then claim the design is scalable. Small data can hide a collection scan, deep pagination cost, or an aggregation that will fail at production volume.

Seed volume

The suggested dataset is large enough to expose compound-index and pagination behavior. Scale it to the available hardware if necessary, but preserve enough tenants, projects, tasks, and comments to make the query selectivity meaningful.

Suggested performance dataset:

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

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

Use generated non-sensitive data.

Cursor pagination requirement

Offset pagination is attractive because it is easy to write, but deep offsets make the database walk past results the client will discard. A cursor describes a position in the endpoint's deterministic sort instead.

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

Cursor must be:

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

Test insertion between pages. The test should demonstrate that the chosen sort and cursor avoid duplicates or missing results, subject to the expected semantics of concurrent changes.

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

Rate limits

Rate limits protect the operations that are expensive, sensitive, or easy to abuse. In a multi-instance deployment, the limiter's state must be shared or distributed; a process-local counter allows the effective limit to multiply with every app instance.

At minimum:

text
login
search
exports
webhook resend/admin

Shared/distributed limiter when multiple app instances.

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

Body limits

Use limits that match the operation rather than one large global limit. Align the reverse proxy and Express settings so an oversized request is rejected consistently before it can consume disproportionate memory or work.

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

File bytes have different lifecycle and security needs from ordinary metadata. Keep large content in object storage by default, and make every download path perform the same tenant and resource authorization as the upload path.

If files included:

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

GridFS is allowed when justified, but it must also be accessed as a stream and governed by explicit size, authorization, and retention rules.

Error contract

Clients need stable public error codes, while operators need the underlying diagnostic context in logs. Keep those concerns separate: map database and upstream failures to the public contract without exposing MongoDB internals, index names, connection details, or URIs.

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 MongoDB stack traces, index names, or connection URIs.

HTTP semantics

Status codes should describe the outcome at the HTTP boundary, and the application error code should make the outcome actionable. Choose the hidden-resource policy deliberately so authorization does not accidentally reveal whether another tenant's resource exists.

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 these semantics consistently across all routes and tests.

Request correlation

Every request needs a stable correlation value that can connect the client response, server log, database activity, and background event. Log route templates rather than raw URLs so query strings cannot accidentally carry secrets into observability systems.

Every request:

text
requestId

log:

text
route template
status
duration
error code
release

Do not log a raw URL that may contain secrets.

Metrics

These metrics should help separate an HTTP problem from a database-capacity problem, an event-loop problem, or a backlog in asynchronous work. Keep high-cardinality tenant and user identifiers out of metric labels; use logs or traces for scoped investigation instead.

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 and user IDs as metrics labels.

Tracing

A trace should make a request's cross-boundary path visible without copying sensitive data into span attributes. The example gives an operator enough context to distinguish authorization time, database time, and outbox work.

Span:

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

Use OpenTelemetry/APM if available.

Sanitize database query attributes before recording them.

Health

Keep liveness and readiness separate. Liveness answers whether the process should remain running; readiness answers whether it should receive traffic and depends on the service's ability to do useful work.

text
/live
/ready

Readiness must become false during shutdown so the load balancer can stop sending new traffic.

A brief MongoDB election should not necessarily kill liveness. It may make readiness fail temporarily, but restarting a healthy process for every short election can turn a dependency event into an application outage.

Graceful shutdown requirement

Shutdown is part of normal deployment behavior. The process must stop taking new work, let safe in-flight work finish or abort, close database resources, and exit within the orchestrator's deadline.

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 the process test. Manual shutdown checks rarely cover the race between readiness changes, worker claims, active transactions, and resource cleanup.

Backup requirement

Recovery is not demonstrated by having a backup policy on paper. Define the recovery point and time the business needs, then restore into an isolated environment and verify that the restored system is usable.

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. Record the actual restore time rather than assuming the documented estimate is accurate.

Schema migration requirement

Schema changes must account for documents written by old and new application versions during rollout. The migration plan should keep reads and writes compatible for the transition window and include a recovery path if validation or the new index causes trouble.

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. A large unbounded rewrite can compete with application traffic and leave a partially migrated dataset when it fails.

Index migration requirement

An index rollout is an operational change, not only a line added to an initialization script. Observe the build, verify that application versions work during the transition, and remove the old index only after evidence shows it is no longer needed.

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

Failure injection turns assumptions into observed behavior. For each case, record what the user sees, what operators can find, what is retried, and whether the durable data remains correct; a swallowed exception is not a recovery strategy.

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 failure, record:

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

Testing requirements

Use the lowest level that proves each behavior, then combine those behaviors at the boundaries where failures actually occur. Unit tests are useful for deterministic rules, but tenant isolation, transaction behavior, indexes, and driver semantics need integration coverage against MongoDB.

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

Security tests should attack the boundaries directly, not just confirm that the happy path works for an authorized user. Include both read and write attempts, malformed operator-shaped input, and replay or authorization cases for every feature that introduces a new resource path.

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 the changes that are expected to affect cost, and keep the dataset and workload fixed enough for a meaningful comparison. A subjective impression that a request “feels faster” is not evidence of an index or aggregation improvement.

Measure before/after:

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

Do not optimize using anecdotal “feels faster.”

Architecture review document

Write down where truth lives and where responsibility changes. This document should let a reviewer follow a request from an untrusted browser through authentication and business rules to durable data, and then understand what happens when an external system or worker fails.

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

These questions are intended to expose gaps in the design, not to test memorized terminology. Be able to answer them using the capstone's own queries, traces, tests, migration notes, and recovery evidence.

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

Use this checklist as a release gate. Mark an item complete only when the implementation and its evidence agree; a design note without a test, or a passing test without a documented operational decision, is not enough for the corresponding requirement.

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.

That is the standard for the capstone: the system should be explainable under normal operation, under load, and after a failure.

Official references

Reader page: /mongodb/lesson/148/node-js-mongodb-production-capstone-multi-tenant-api-query-plans-transactions-search-tests-security-and-recovery