cswork / libraryOpen practice workspace

Articles

Mongoose Interview Questions: Validation, Middleware, and Update Semantics

Answer Mongoose interview questions on document and update validation, query middleware, findOneAndUpdate, and concurrent invariants with examples.

A Mongoose schema can contain a validator, a pre-save hook, and a unique field—and still allow an update path that behaves nothing like a document save. That is the interview trap. To answer Mongoose questions well, name the operation first, then trace which validation and middleware path it actually takes. “The schema handles it” is not a sufficient guarantee.

Official fact: current Mongoose validation documentation says update validators are off by default and, when enabled, apply only to updated paths. Its middleware guide says save hooks do not run for query updates. Inference: review write APIs as separate contracts and test each one. The examples here are original diagnostic exercises, not accounts of a specific employer's interview.

Question 1: What does document validation actually cover?

A Mongoose document created from a model has schema paths, casting, defaults, and validation behavior. Document validation runs when you call validate or save, and Mongoose registers validation as part of the save lifecycle. A custom validator may inspect another property through this because, for document validation, this is the document. A good answer also separates application validation from database constraints: a Mongoose validator catches certain invalid values in this code path, but another client can write directly to MongoDB without running that JavaScript validator.

Suppose a product has currency and price fields, and a custom validator requires a positive price for active products. Saving a loaded Product can evaluate its document state. If the validator uses this.currency or this.status, it is reading document fields. A test should create a valid product, mutate one field, call validate or save, and assert the exact validation failure. It should also exercise a valid state to avoid a validator that always rejects.

Do not say that unique: true is a normal Mongoose validator. The validation guide explicitly distinguishes the unique option from validators; it creates a unique index. A duplicate value can surface as a MongoDB duplicate-key error, so catch and classify it separately from a Mongoose ValidationError. For a concurrent signup or SKU creation, the database index—not a preceding “does it exist?” query—must enforce uniqueness. Reason About Nested JSON Schema Validation provides useful practice in stating precisely what a schema rule does and does not prove.

Question 2: Why can updateOne bypass the rule you expected?

Model.updateOne is a query update. By default, update validators are off. Passing runValidators: true turns on the supported update-validation path, but it still does not reconstruct and validate the entire resulting document. Mongoose documents that update validators run on updated paths, and required validators on update generally fail when a required path is explicitly unset. A patch that changes only price may not rerun a validator on untouched currency or status. The exact validator behavior also depends on the update operator, so inspect the operation instead of speaking about “updates” as a single category.

The interview answer should be a trace. Start with the filter, update document, options, schema validators, and existing record. Ask which paths the update mentions. Ask whether runValidators is set. Ask whether a cross-field validator expects a document but will receive a query. Then ask whether a database constraint is required even after Mongoose checks. That sequence catches more bugs than simply adding runValidators to every call.

Here is a deliberately small comparison:

OperationValidation and hook boundaryWhat to inspect
doc.save()Document validation and save middlewareDocument state, modified paths, ownership of the loaded record
Model.updateOne()Query update; validators require runValidatorsUpdated paths and update operators
Model.findOneAndUpdate()Query update; distinct query hooksReturned before/after image and atomic filter
Direct MongoDB writeMongoose rules are bypassedDatabase indexes and collection-level rules

This table is a decision aid, not a replacement for reading the versioned API. If you need a cross-field invariant over the final record, a patch-path validator may be insufficient. Options include loading and saving the document with a concurrency strategy, placing the condition in the atomic update filter, using a transaction where appropriate, or enforcing the invariant at the database boundary. The right choice depends on races and write volume; “always save first” is not universally safe.

Question 3: Why does this point to a query in an update validator?

Mongoose's update-validator documentation gives the key distinction: in document validation, this refers to the document; in update validation, this refers to the Query object. A function that checks this.status may be correct on save but wrong during update. Within a query validator, inspect values being updated through query methods such as this.get('status') when appropriate. That still does not hand you the entire post-update document.

For example, imagine a color rule tied to the product name. On save, this.name can be read from the document. On updateOne with a change only to color, the query may contain no new name. Reading this.get('name') yields only the update's name value when present; it is not necessarily the persisted name. Pretending the query is a hydrated document produces incorrect validation. This is why cross-field invariants need explicit design instead of a clever validator callback.

An interview follow-up may ask whether fetching the document inside the validator solves it. It may allow comparison, but a separate read can become stale before the update commits. If the invariant must hold under concurrent writes, express it as an atomic condition or use an appropriate transaction or database constraint. Defend a Project's Schema, Indexes, and Concurrency Model is a relevant rehearsal because the difficult part is the invariant, not the API name.

Question 4: Which middleware runs?

Mongoose distinguishes document, query, model, and aggregate middleware. A pre('save') hook belongs to the document save path; it does not run on Model.updateOne or findOneAndUpdate. Mongoose registers updateOne middleware as query middleware by default, including when doc.updateOne is called, so this in that hook is a query unless you explicitly register a document middleware variant. A pre('findOneAndUpdate') hook similarly sees a query, not the document being changed.

This matters for audit fields, derived values, and side effects. Suppose a pre-save hook normalizes an email to lowercase. A new document saved through doc.save gets the normalization. A code path calling Model.updateOne with an email patch will not run that save hook. If the index or query assumes lowercase, the records diverge. You can normalize at a shared input boundary, add appropriate query middleware, or use a database strategy; whichever route you choose, test every write path used by the service.

Do not assume a hook can safely perform an external irreversible side effect before the database operation commits. A pre hook can fail the operation, and an external call may already have happened. A post hook can observe a successful method call but cannot automatically make a remote side effect atomic with MongoDB. If the question involves payments or messages, describe an outbox or recoverable work queue rather than promising hook magic. Design a Merchant Catalog Update Pipeline with Bulk Files and Image Resizing offers a useful scenario for separating data changes from asynchronous side effects.

Question 5: What does findOneAndUpdate return, and when use it?

The current Mongoose findOneAndUpdate tutorial explains that Mongoose returns the document before the update by default. Use returnDocument: 'after' when your application needs the new document. That differs from expecting the same return shape as the underlying MongoDB Node driver. The tutorial also notes an atomicity advantage over load-mutate-save for a single document, with an unindexed-upsert exception. Atomicity of that command does not mean your whole business workflow is atomic.

Consider a stock decrement. A naïve flow loads a product, checks stock greater than zero, decrements, and saves. Two clients can both pass the check before either save, depending on concurrency behavior. A stronger one-document update includes stock: { $gte: 1 } in the filter and $inc: { stock: -1 } in the update, then checks whether a document matched. If the request also creates an order in another collection, you still need a strategy for the two-record invariant. The one-document atomic update only covers its own boundary.

Also discuss upsert carefully. If the filter does not use an appropriate unique/indexed key, concurrent upserts can produce surprising duplicates or lose the assumed atomic guarantee. An interviewer may ask whether “find and then insert” is equivalent; it is not under concurrency without an enforcing index or transaction. The correct answer identifies the database key and what happens when two callers race.

A worked diagnosis: invalid SKU state after a patch

Imagine an inventory service with a Product schema containing status, price, and sku. It validates price is nonnegative on save, sets a normalized SKU in pre('save'), and declares sku unique. A PATCH endpoint uses Model.findOneAndUpdate(filter, { $set: req.body }) without runValidators and returns the result. After a release, a product appears with a negative price, mixed-case SKU, and an API response that shows the old document. Three distinct mechanisms explain the symptoms.

First, the query update did not opt into update validators, so the price rule did not run. Second, pre-save normalization did not execute because findOneAndUpdate follows query middleware, not save middleware. Third, the default return document is the pre-update image, so the HTTP response appears stale even if the database changed. The unique index only addresses duplicate indexed values; it does not normalize case unless its index design explicitly handles that need. One setting cannot repair all three symptoms.

A disciplined fix starts with the contract. Which fields may the client patch? Validate and normalize those inputs explicitly. Set runValidators where it applies, but do not mistake it for full-document validation. Decide whether a case-insensitive uniqueness rule belongs in an indexed normalized field or suitable collation, then verify the index exists. Request the post-update document if the response promises it. Add tests for updateOne, findOneAndUpdate, and save, including a concurrent duplicate-SKU attempt. The goal is an invariant that survives every authorized writer, not a green unit test for one hook.

Design a JSON Schema Validator helps practice schema rules, while Design a String-Rule Validator for Transaction Records prompts you to separate input rules from persistence rules.

How to answer the interviewer efficiently

Start your answer by writing the exact API call on the board. Label it document or query operation. State whether validation is enabled, which paths it sees, which middleware family fires, what this refers to, and whether the database itself enforces the invariant. For a returned document, say before or after. For concurrency, identify the atomic unit. Only then propose a fix.

This method avoids two common overstatements: “Mongoose validates every write” and “findOneAndUpdate makes the entire workflow safe.” The documentation supports a narrower, more useful conclusion: different write paths have different semantics, and robust applications choose them deliberately.

Sources and further reading