Sling Academy
Home/Node.js/How to use UUID schema type in Mongoose

How to use UUID schema type in Mongoose

Last updated: December 30, 2023

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.

Next Article: Understanding the Map schema type in Mongoose

Previous Article: Virtuals in Mongoose: Explained with Examples

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