When working with MongoDB and NestJS, ensuring the uniqueness of data fields can be challenging, especially when dealing with conditional constraints. A common requirement is to enforce unique email addresses only when the user account is active. This cannot be achieved directly using Mongoose’s unique: true
constraint but can be handled effectively through several approaches.
Solution 1: Using a Pre-Save Hook
The pre-save hook allows checking for existing active email addresses before saving a new document. Here’s how you can modify your schema:
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import mongoose, { Document } from 'mongoose';
@Schema()
export class User extends Document {
@Prop({ required: true })
fullName: string;
@Prop({ required: true, maxlength: 50 })
email: string;
@Prop({ required: true })
password: string;
@Prop({ required: true, enum: ['active', 'inactive'], default: 'active' })
accountStatus: string;
}
export const UserSchema = SchemaFactory.createForClass(User);
UserSchema.pre<User>('save', async function (next) {
if (this.accountStatus === 'active') {
const existingUser = await mongoose.model('User').findOne({
email: this.email,
accountStatus: 'active',
_id: { $ne: this._id },
});
if (existingUser) {
const error = new Error('Email must be unique for active users');
return next(error);
}
}
next();
});
Explanation:
- The
pre-save
hook checks if there's another user with the same email andactive
status. _id: { $ne: this._id }
excludes the current document when updating.
Solution 2: Using a Custom Validation Function
Another way to enforce uniqueness is through a custom validator inside the schema definition:
@Prop({
required: true,
validate: {
validator: async function (value: string) {
const existingUser = await mongoose.model('User').findOne({
email: value,
accountStatus: 'active',
_id: { $ne: this._id },
});
return !existingUser;
},
message: 'Email must be unique for active users',
},
})
email: string;
Explanation:
- The validator queries the database to check for duplicate active email addresses.
- If a duplicate exists, the validation fails with a custom message.
Solution 3: Enforce Uniqueness at the Service Level
Enforcing the constraint at the service level provides more control over the logic.
async createUser(createDto: CreateUserDto) {
const existingUser = await this.userModel.findOne({
email: createDto.email,
accountStatus: 'active',
});
if (existingUser) {
throw new HttpException('Email must be unique for active users', 400);
}
return await this.userModel.create(createDto);
}
Explanation:
- The service method checks for existing records before proceeding with the creation.
Solution 4: Using Compound Index for Conditional Uniqueness
Adding a compound index ensures database-level enforcement of uniqueness based on conditions.
export const UserSchema = SchemaFactory.createForClass(User);
UserSchema.index(
{ email: 1, accountStatus: 1 },
{ unique: true, partialFilterExpression: { accountStatus: 'active' } }
);
Explanation:
- The compound index applies uniqueness only when the account status is
active
. partialFilterExpression
ensures the index works conditionally.
Steps to Verify:
1. Ensure the index is created in the database.2. Manually check the index in MongoDB:
db.users.getIndexes()
3. Test by inserting duplicate emails with different statuses.
Best Practices for Managing Unique Fields in MongoDB
1. Choose the right enforcement level:- Schema-level (pre-save hooks, validation) is good for small to medium complexity projects.
- Service-level enforcement offers flexibility and custom logic.
- Database-level indexing provides high performance and scalability.
2. Testing:
- Regularly test your constraints to ensure they behave as expected.
- Write unit tests to validate unique field constraints.
3. Performance considerations:
- Use indexing carefully, as too many indexes can slow down write operations.
- Monitor database performance using MongoDB tools.
Final Thoughts
Choosing the right approach depends on your project’s complexity:
- Pre-save hook: Ideal for automatic checks within the schema.
- Custom validation: Works well for more granular control.
- Service-level enforcement: Best for complex application logic.
- Compound index: Recommended for performance and ensuring uniqueness at the database level.
By understanding these approaches, you can effectively manage unique fields in MongoDB with NestJS, ensuring data consistency and reliability in your application.
Let me know if you have any questions or need further guidance! ๐