Sling Academy
Home/Node.js/Mongoose: How to use ‘id’ instead of ‘_id’

Mongoose: How to use ‘id’ instead of ‘_id’

Last updated: December 30, 2023

Introduction

Working with MongoDB typically involves dealing with _id field, which is a BSON type provided by MongoDB to uniquely identify documents. Mongoose, a popular Object Document Mapper (ODM) for MongoDB, abides by this convention but also offers a virtual id field to make working with the identifiers in application code more comfortable and familiar, especially for those coming from JSON-centric systems or ORMs that use id without an underscore. This tutorial provides an in-depth look into configuring Mongoose schemas to work with a virtual id field.

Configuring the Mongoose Schema

Before diving into advanced configurations, let’s start with a basic Mongoose model example:

const mongoose = require('mongoose');

const Schema = mongoose.Schema;
const ObjectId = Schema.Types.ObjectId;

const exampleSchema = new Schema({
  name: { type: String, required: true },
  // Other fields...
});

const Example = mongoose.model('Example', exampleSchema);

module.exports = Example;

As you can see, the schema defines a model for documents that will have a name field. By default, Mongoose adds an _id field of ObjectId type to the schema to act as the primary identifier.

To make the id without an underscore accessible, we can use Mongoose’s built-in virtual properties. These virtuals are not persisted to MongoDB — they only exist logically and can be used as if they were fields in the model.

Let’s modify the schema to include a virtual id:

exampleSchema.virtual('id').get(function () {
  return this._id.toHexString();
});

exampleSchema.set('toJSON', {
  virtuals: true
});

exampleSchema.set('toObject', {
  virtuals: true
});

By adding these lines of code, we tell Mongoose to convert the _id field to string and provide it as id. Also, we configure the schema to include virtuals when we convert documents to JSON or plain objects.

Considerations with Saving References

When dealing with related collections, it’s typical to save references to other documents via their _id. If you are using the id getter in your application logic, you should be mindful that operations expecting an _id, such as populate() or querying, will still work with a string representation thanks to type coercion in BSON.

Handling Virtuals with Mongoose Methods

Not all methods treat virtuals the same, hence when using methods as lean(), you must explicitly state that virtuals should be included, as such:

Example.find().lean({ virtuals: true }).then(examples => {
  examples.forEach(example => console.log(example.id)); // Now 'id' is accessible
}).catch(err => {
  console.error('Query failed', err);
});

Advanced Use Cases

Advanced use cases might involve using the Mongoose alias feature or manipulating the ObjectID field itself. In some cases, development teams might want to use their own custom logic to generate unique keys that replace the traditional _id for specific reasons.

For more sophisticated solutions custom modifications using pre and post hooks or middleware might be necesary. Some utilities of these would include automatic translation of id to _id during update operations, or pre-saving unique identifier generation that differs from the default ObjectId styles.

Sample Middleware Injection

Below is an example of a pre-save hook to inject custom IDs before document saving:

exampleSchema.pre('save', function(next) {
  if (!this.isNew) {
    next();
    return;
  }
  this._id = generateCustomId(); // Assumed to be implemented elsewhere
  next();
});

Conclusion

While Mongoose automatically works with MongoDB’s _id field on the back end, developers can definitely streamline their front-end code by using virtual fields to reference model identifiers as id. This tutorial has touched upon how one can configure their Mongoose schemas to include such virtual fields and has explored the extent and limitations of their application within various areas of Mongoose’s feature set.

It is highly encouraged that application developers understand not just the how, but also the when and why of overriding default behaviors. Leveraging the Mongoose library effectively ensures your application’s data models are both robust and intuitive.

Next Article: Fixing Mongoose Error: ISODate is not defined in Node.js

Previous Article: Cannot connect to MongoDB Atlas with Mongoose: How to Fix

Series: Mongoose.js Tutorials

Node.js

You May Also Like

  • NestJS: How to create cursor-based pagination (2 examples)
  • Cursor-Based Pagination in SequelizeJS: Practical Examples
  • MongooseJS: Cursor-Based Pagination Examples
  • Node.js: How to get location from IP address (3 approaches)
  • SequelizeJS: How to reset auto-increment ID after deleting records
  • SequelizeJS: Grouping Results by Multiple Columns
  • NestJS: Using Faker.js to populate database (for testing)
  • NodeJS: Search and download images by keyword from Unsplash API
  • NestJS: Generate N random users using Faker.js
  • Sequelize Upsert: How to insert or update a record in one query
  • NodeJS: Declaring types when using dotenv with TypeScript
  • Using ExpressJS and Multer with TypeScript
  • NodeJS: Link to static assets (JS, CSS) in Pug templates
  • NodeJS: How to use mixins in Pug templates
  • NodeJS: Displaying images and links in Pug templates
  • ExpressJS + Pug: How to use loops to render array data
  • ExpressJS: Using MORGAN to Log HTTP Requests
  • NodeJS: Using express-fileupload to simply upload files
  • ExpressJS: How to render JSON in Pug templates