How to create auto-incrementing field in Mongoose

Updated: December 30, 2023 By: Guest Contributor Post a comment

Introduction

Building applications with databases often necessitates the use of unique identifiers that increment with each new record. In SQL databases, this is typically handled out of the box. However, in MongoDB, which Mongoose is built to interface with, such a feature isn’t provided by default. In this tutorial, we’ll explore how to implement an auto-incrementing field in a Mongoose schema by using several code examples of increasing complexity.

Auto-increment can be particularly useful when we want to create user-friendly identifiers for records that are easier to refer to than the default ObjectId that MongoDB generates. We’ll discuss how you can create auto-incrementing fields in Mongoose schemas for performing functionalities like keeping track of the number of users, items, or a sequential order of records in any collection.

Before we begin, make sure you have:

  • Node.js and npm (Node Package Manager) installed
  • A basic understanding of JavaScript or TypeScript’s latest syntax
  • Some experience working with Mongoose over MongoDB

Setup

Let’s start with a basic Mongoose setup:

import mongoose from 'mongoose';

mongoose.connect('mongodb://localhost/exampleDb', {
  useNewUrlParser: true,
  useUnifiedTopology: true
});

Auto-Increment with Mongoose Plugin

To begin, we’ll look into using a plugin that can be added to any schema to provide auto-increment functionality:

import mongoose from 'mongoose';
import autoIncrement from 'mongoose-auto-increment';

const connection = mongoose.createConnection('mongodb://localhost/exampleDb');

autoIncrement.initialize(connection);

const UserSchema = new mongoose.Schema({
  //... other fields ...
  userId: Number
});

UserSchema.plugin(autoIncrement.plugin, 'User');
const User = connection.model('User', UserSchema);
// `userId` will auto-increment

This is a great starting point and perfect for relatively simple use cases. However, it doesn’t allow much customization and may be limiting as our application grows.

The Custom Counter Approach

For a more flexible solution, one can create a counter schema:

import mongoose from 'mongoose';

const CounterSchema = new mongoose.Schema({
  _id: String,
  seq: Number
});

CounterSchema.methods.increment = async function() {
  this.seq += 1;
  return this.save();
};

const Counter = mongoose.model('Counter', CounterSchema);

Counter.findOneAndUpdate(
  { _id: 'userId' },
  { $inc: { seq: 1 } },
  { new: true, upsert: true }
)
.catch(console.error);

Then, before creating a new User record, we increment the counter to generate a new unique identifier:

const UserSchema = new mongoose.Schema({
  // ... other fields ...
  userId: Number
});

UserSchema.pre('save', async function() {
  if (this.isNew) {
    const counter = await Counter.findOneAndUpdate(
      { _id: 'userId' }, { $inc: { seq: 1 } },
      { new: true, upsert: true }
    );
    this.userId = counter.seq;
  }
});

const User = mongoose.model('User', UserSchema);

This way, the `userId` field increments every time a new User document is saved. This approach gives more control over how counter increments.

Auto-Incrementing Sequence in Transactions

For applications requiring robust transaction support, ensuring that sequences are not duplicated even if documents are not successfully created can be crucial. Transactions can be combined with change streams to lock the counter update process till they’re complete:

//... Initializing transaction ...
const session = await connection.startSession();
session.startTransaction();

// Performing counter increment strictly within a transaction
const opts = { session, new: true };
const counter = await Counter.findOneAndUpdate(
  { _id: 'userId' }, { $inc: { seq: 1 } }, opts
);

try {
  const newUser = User.create([{ userId: counter.seq }], { session: session });
  await session.commitTransaction();
} catch (error) {
  await session.abortTransaction();
  throw error;
} finally {
  session.endSession();
}

This way, even if the User document creation encounters a problem, the userId counter would be incremented safely as a part of the same transaction.

Conclusion

In this tutorial, we have traversed through ways to add auto-increment functionality to your Mongoose models, going from plug-and-play techniques to more complex, custom implementations suitable for production-scale applications. Remember, a comprehensively tested implementation reduces the risk of duplicates or other issues in fields that are expected to auto-increment reliably. Ultimately, what approach you choose will depend on the specific needs and constraints of your application. Happy Coding!