How to use UUID schema type in Mongoose

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

Introduction

Mongoose is a popular Object Data Modeling (ODM) library for MongoDB and Node.js. It manages relationships between data, provides schema validation, and translates between objects in code and their MongoDB representations. In this article, we’ll delve into using UUIDs as schema types in Mongoose, offering a robust way to handle unique identifiers across distributed systems.

Understanding UUID

UUID (Universal Unique Identifier) is a standardized 128-bit format for a string ID to ensure uniqueness across different databases and systems. Mongoose doesn’t natively support UUIDs, but it can easily be extended to do so. Using UUIDs can be highly beneficial when you want a guaranteed unique identifier not tied to a particular DB’s auto-generated IDs. This is particularly useful in systems that require a high level of data consistency and unique identification across multiple databases.

Before proceeding, you must have Node.js and MongoDB installed on your system, as well as the Mongoose library. If you haven’t installed Mongoose, you can add it to your project using npm or Yarn:

npm install mongoose
// or
yarn add mongoose

We will also need a library to generate UUIDs in our application:

npm install uuid
// or
yarn add uuid

Basic Mongoose UUID Usage

First, let’s create a basic Mongoose model that uses UUID as its ID type. We will use the uuid npm package to generate UUIDs:

import mongoose from 'mongoose';
import { v4 as uuidv4 } from 'uuid';

const userSchema = new mongoose.Schema({
  _id: { type: String, default: () => uuidv4().replace(/\-/g, '') },
  name: String,
  email: String
});

const User = mongoose.model('User', userSchema);
export default User;

Advanced Usage

For a more advanced UUID scenario, consider situations where you impose constraints like version-specific UUID or where an existing database returns UUIDs in a particular format that you have to reformat before saving to your MongoDB database.

import mongoose from 'mongoose';
import { parse as uuidParse, stringify as uuidStringify } from 'uuid';

const userSchema = new mongoose.Schema({
  _id: {
    type: mongoose.SchemaTypes.Buffer,
    subtype: 4,
    default: function () {
      // Parse and stringify the UUID ensure the format
      return Buffer.from(uuidParse(uuidv4()));
    }
  },
  // additional fields
});

userSchema.virtual('uuid').get(function () {
  return uuidStringify(new Uint8Array(this._id.buffer));
});

// Convert _id to a UUID string when converting to JSON
userSchema.set('toJSON', {
  virtuals: true,
  versionKey: false,
  transform: function (doc, ret) {
    ret.id = ret.uuid;
    delete ret._id;
    delete ret.uuid;
  }
});

const User = mongoose.model('User', userSchema);
export default User;

Working with Raw UUID Data

It’s possible to directly work with raw UUID bytes. This is particularly performant for certain operations, as string formats consume more space and take longer to compare.

const mongoose = require('mongoose');
const { Schema } = mongoose;

// Connect to MongoDB (ensure you have MongoDB running)
mongoose.connect('mongodb://localhost:27017/testDB', { useNewUrlParser: true, useUnifiedTopology: true });

// Define a schema with field aliases
const userSchema = new Schema({
  uName: { type: String, alias: 'username' },
  pWord: { type: String, alias: 'password' }
});

// Create a model from the schema
const User = mongoose.model('User', userSchema);

// Create a new user instance using aliases
const newUser = new User({
  username: 'johndoe',
  password: '12345'
});

// Save the user to the database
newUser.save((err, user) => {
  if (err) return console.error(err);
  console.log('Saved user:', user);
  mongoose.connection.close();
});

// When retrieving or working with the user, you can use aliases
User.findOne({ username: 'johndoe' }).exec((err, user) => {
  if (err) return console.error(err);
  console.log('Found user:', user);
  mongoose.connection.close();
});

In this example:

  • A Mongoose schema for a User is defined with two fields: uName and pWord.
  • Aliases username and password are assigned to these fields, respectively.
  • When creating a new User instance, you can use the aliases username and password instead of the actual schema field names uName and pWord.
  • The same applies when querying the database – you can use the aliases in your query conditions.

Error Handling

You must also consider error handling, especially when dealing with unique constraints and possible collision handling mechanisms.

import mongoose from 'mongoose';
import { v4 as uuidv4 } from 'uuid';
import { Schema } from 'mongoose';

// Connect to MongoDB
mongoose.connect('mongodb://localhost:27017/myDatabase', { useNewUrlParser: true, useUnifiedTopology: true });

// User schema with a UUID field
const userSchema = new Schema({
  uuid: { 
    type: String, 
    unique: true, // Ensuring the uuid is unique
    default: () => uuidv4() // Automatically generate a UUID
  },
  name: String
});

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

// Create a new user
const newUser = new User({ name: 'John Doe' });

newUser.save(function(err) {
  if (err) {
    if (err.code === 11000) {
      // Handle the duplicate key error (unique constraint violation)
      console.error('UUID collision detected or duplicate entry found');
    } else {
      // Handle other errors
      console.error('Error occurred:', err.message);
    }
  } else {
    console.log('User saved successfully');
  }
});

In this code:

  • The uuid field is set to be unique. If a UUID collision or any other violation of the unique constraint occurs (though highly unlikely with UUIDs), it will result in a MongoDB duplicate key error (error code 11000).
  • The save method includes error handling to catch and identify a duplicate key error versus other types of errors.
  • A default value is set for the uuid field, automatically generating a UUID for each new user.

Remember, UUID collisions are extremely rare, but this setup helps you handle any such occurrences or other database-related errors gracefully (especially when you’re working with large systems like banking cores or a popular social networks).

Conclusion

In this article, we covered how to define UUIDs in your Mongoose schema, from simple setups to more advanced configurations. By utilizing UUIDs, you can ensure that your records have unique identifiers that are not tied to the particular internals of your MongoDB NoSQL database, making your applications more flexible and robust in distributed systems. This knowledge serves as a foundation that you can expand upon as you grow your Node.js applications.