Understanding `unique` in Mongoose

Jul 13, 2020

The unique option tells Mongoose that each document must have a unique value for a given path. For example, below is how you can tell Mongoose that a user's email must be unique.

const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  email: {
    type: String,
    unique: true // `email` must be unique
  }
});
const User = mongoose.model('User', userSchema);

If you try to create two users with the same email, you'll get a duplicate key error.

// Throws `MongoError: E11000 duplicate key error collection...`
await User.create([
  { email: 'test@google.com' },
  { email: 'test@google.com' }
]);

const doc = new User({ email: 'test@google.com' });
// Throws `MongoError: E11000 duplicate key error collection...`
await doc.save();

Updates can also throw a duplicate key error. For example, if you create a user with a unique email address and then update their email address to a non-unique value, you'll get the same error.

await User.create({ email: 'test2@google.com' });

// Throws `MongoError: E11000 duplicate key error collection...`
await User.updateOne({ email: 'test2@google.com' }, { email: 'test@google.com' });

Index, Not Validator

A common gotcha is that the unique option tells Mongoose to define a unique index. That means Mongoose does not check uniqueness when you use validate().

await User.create({ email: 'sergey@google.com' });

const doc = new User({ email: 'sergey@google.com' });
await doc.validate(); // Does not throw an error

The fact that unique defines an index as opposed to a validator is also important when writing automated tests. If you drop the database the User model is connected to, you'll also delete the unique index, and you will be able to save duplicates.

await mongoose.connection.dropDatabase();

// Succeeds because the `unique` index is gone!
await User.create([
  { email: 'sergey@google.com' },
  { email: 'sergey@google.com' }
]);

In production you normally wouldn't drop the database, so this is rarely an issue in production.

When writing Mongoose tests, we normally recommend using deleteMany() to clear out data in between tests, rather than dropDatabase(). This ensures that you delete all documents, without clearing out database-level configuration, like indexes and collations. deleteMany() is also much faster than dropDatabase().

However, if you choose to drop the database between tests, you can use the Model.syncIndexes() function to rebuild all unique indexes.

await mongoose.connection.dropDatabase();

// Rebuild all indexes
await User.syncIndexes();

// Throws `MongoError: E11000 duplicate key error collection...`
await User.create([
  { email: 'sergey@google.com' },
  { email: 'sergey@google.com' }
]);

Handling null Values

Since null is a distinct value, you cannot save two users that have a null email. Similarly, you cannot save two users that don't have an email property.

// Throws because both documents have undefined `email`
await User.create([
  {},
  {}
]);

// Throws because both documents have null `email`
await User.create([
  { email: null },
  { email: null }
]);

One workaround is to make the email property required, which disallows null and undefined:

const userSchema = new mongoose.Schema({
  email: {
    type: String,
    required: true,
    unique: true // `email` must be unique
  }
});

If you need email to be unique unless it is not defined, you can instead define a sparse unique index on email as shown below.

const userSchema = new mongoose.Schema({
  email: {
    type: String,
    // `email` must be unique, unless it isn't defined
    index: { unique: true, sparse: true }
  }
});

User-Friendly Duplicate Key Errors

To make MongoDB E11000 error messages user-friendly, you should use the mongoose-beautiful-unique-validation plugin.

const schema = new Schema({ name: String });
schema.plugin(require('mongoose-beautiful-unique-validation'));

const CharacterModel = mongoose.model('Character', schema);

const doc = await CharacterModel.create({ name: 'Jon Snow' });

try {
  // Try to create a document with the same `_id`. This will always fail
  // because MongoDB collections always have a unique index on `_id`.
  await CharacterModel.create(Object.assign({}, doc.toObject()));
} catch (error) {
  // Path `_id` (5cc60c5603a95a15cfb9204d) is not unique.
  error.errors['_id'].message;
}

Want to become your team's MongoDB expert? "Mastering Mongoose" distills 8 years of hard-earned lessons building Mongoose apps at scale into 153 pages. That means you can learn what you need to know to build production-ready full-stack apps with Node.js and MongoDB in a few days. Get your copy!

More Mongoose Tutorials