Using Multiple MongoDB Databases in a Single Server with Node.js and TypeScript

Chukwuemeka Maduekwe - Feb 12 - - Dev Community

Setting up multiple MongoDB databases in a single Typescript Nodejs Server somes with a lot of advantages, ranging from:

  1. Data segregation
  2. Scalability and performance
  3. Security and access control
  4. Data isolation
  5. Third-party integration
  6. Testing and development
  7. Future-proofing and modularity

NB: Here's a link to the repo where I'm actively using multiple databases in production WaveRD just in case you need a reference to learn from.
In this application what I did was to create a separate databases for Accounts (profile creation, signin signup, etc.), Managers (club, player, manager data in WaveRD - Soccer Manager) Console (App logs, contact messages, advertisment, error logs, etc. ) and ApiHub (Data for all public endpoints).
In this application I used mongoose as the ORM, but just in case you need to use MongoDB methods directly, that should also work.

// profile schema

import bcrypt from "bcryptjs";
import { Schema } from "mongoose";

import { accountsDatabase } from "../database";
import { generateSession } from "../../utils/handlers";

const ProfileSchema = new Schema(
  {
    name: { type: String, required: true },
    created: { type: Date, default: Date.now() },
    email: { type: String, unique: true, lowercase: true, trim: true, required: false },
    role: { type: String, default: "user" }, // ? user || admin
    status: { type: String, default: "active" }, // ? active || suspended
    avatar: { type: String, default: "/images/layout/profile.webp" }, // ? avatar web location
    handle: { type: String, required: true }, // ? Unique but we don't want index created on this
    theme: { type: String, required: true },
    auth: {
      session: { type: String },
      locked: { type: Date, default: null },
      deletion: { type: Date, default: null },
      password: { type: String, required: true },
      inactivity: { type: Date, default: null },

      failedAttempts: {
        counter: { type: Number, default: 0 },
        lastAttempt: { type: Date, default: null },
      },
      lastLogin: {
        counter: { type: Number, default: 0 },
        lastAttempt: { type: Date, default: Date.now() },
      },
      otp: {
        code: { type: String, default: null },
        purpose: { type: String, default: "" },
        time: { type: Date, default: "" },
      },
      verification: {
        email: { type: Date, default: null },
      },
    },
  },
  {
    statics: {
      async comparePassword(attempt: string, password: string) {
        return await bcrypt.compare(attempt, password);
      },
      async hashPassword(password: string) {
        return await bcrypt.hash(password, 10);
      },
    },
  }
);

ProfileSchema.pre("save", async function (next) {
  try {
    if (this.auth && this.auth.otp && this.isModified("auth.password")) {
      this.auth.session = generateSession(this.id); // <= generate login session
      this.auth.otp = { code: generateSession(this.id), purpose: "email verification", time: new Date() };
      this.auth.password = await bcrypt.hash(this.auth.password, 10); // <= Hash password if its a new account
    }
    return next();
  } catch (err: any) {
    return next(err);
  }
});

const ProfileModel = accountsDatabase.model("Profiles", ProfileSchema);

export default ProfileModel;

Enter fullscreen mode Exit fullscreen mode

We create our schema using mongoose like in most app, the code is self explanatory, but just to higlight a few stuff. ProfileSchema describes the structure of our data in Profile collection, while all methods in profile schema statics can be called directly from using the profile model. The pre method attached to ProfileSchema intercepts all records and can manipulate them before they are saved to the database. Finally we export our model for use in our app. The code above can be found on github here just in case its updated in the future.
You can find more Schemas and Models in the models folder inside the src file.

// database.ts

import mongoose from "mongoose";

mongoose.Promise = global.Promise;

mongoose.set({
  // useNewUrlParser: true,
  // useUnifiedTopology: true,
  strictQuery: true,
  debug: false, // ? <= hide console messages

  // keepAlive: true,
  // useNewUrlParser: true,
  // useUnifiedTopology: true,
  // useFindAndModify: false,
});

interface IConnectionEvents {
  all: string;
  open: string;
  close: string;
  error: string;
  connected: string;
  fullsetup: string;
  connecting: string;
  reconnected: string;
  disconnected: string;
  disconnecting: string;
}

const connectionEvents: IConnectionEvents = {
  connecting: "Emitted when Mongoose starts making its initial connection to the MongoDB server",
  connected:
    "Emitted when Mongoose successfully makes its initial connection to the MongoDB server, or when Mongoose reconnects after losing connectivity. May be emitted multiple times if Mongoose loses connectivity.",
  open: "Emitted after 'connected' and onOpen is executed on all of this connection's models.",
  disconnecting: "Your app called Connection#close() to disconnect from MongoDB",
  disconnected:
    "Emitted when Mongoose lost connection to the MongoDB server. This event may be due to your code explicitly closing the connection, the database server crashing, or network connectivity issues.",
  close:
    "Emitted after Connection#close() successfully closes the connection. If you call conn.close(), you'll get both a 'disconnected' event and a 'close' event.",
  reconnected:
    "Emitted if Mongoose lost connectivity to MongoDB and successfully reconnected. Mongoose attempts to automatically reconnect when it loses connection to the database.",
  error: "Emitted if an error occurs on a connection, like a parseError due to malformed data or a data larger than 16MB.",
  fullsetup: "Emitted when you're connecting to a replica set and Mongoose has successfully connected to the primary and at least one secondary.",
  all: "Emitted when you're connecting to a replica set and Mongoose has successfully connected to all servers specified in your connection string.",
};

type LogMessage = { label: string; event: string };
const logMessage = ({ label, event }: LogMessage) =>
  `MongoDB ${label} Database Connection Events} ::: ${connectionEvents[event as keyof IConnectionEvents]}`;

type ModelGenerator = { label: string; db: string };
const modelGenerator = ({ label, db }: ModelGenerator) => {
  return mongoose
    .createConnection(<string>process.env[`${label}_MONGODB_URI`], { dbName: db })
    .on("all", () => logMessage({ label, event: "all" }))
    .on("open", () => logMessage({ label, event: "open" }))
    .on("error", () => logMessage({ label, event: "error" }))
    .on("close", () => logMessage({ label, event: "close" }))
    .on("connected", () => logMessage({ label, event: "connected" }))
    .on("fullsetup", () => logMessage({ label, event: "fullsetup" }))
    .on("connecting", () => logMessage({ label, event: "connecting" }))
    .on("reconnected", () => logMessage({ label, event: "reconnected" }))
    .on("disconnected", () => logMessage({ label, event: "disconnected" }))
    .on("disconnecting", () => logMessage({ label, event: "disconnecting" }));
};

const infoDatabase = modelGenerator({ label: "INFO", db: "info" }); // ? <= Client Database
const gamesDatabase = modelGenerator({ label: "GAMES", db: "games" }); // ? <= Games Database
const apihubDatabase = modelGenerator({ label: "APIHUB", db: "apihub" }); // ? <= API Hub Database
const accountsDatabase = modelGenerator({ label: "ACCOUNTS", db: "accounts" }); // ? <= Auth/Accounts  Database
const federatedDatabase = modelGenerator({ label: "FEDERATED", db: "WaveRD" }); // ? <= Federation Instance Database

export { accountsDatabase, infoDatabase, apihubDatabase, gamesDatabase, federatedDatabase };


Enter fullscreen mode Exit fullscreen mode

This code actually creates a connection event for our databases, and can also be used to log data here. Inside the snippet above we connect to our different databases and export them for use in our app. The code can be found here.

. . . . .