Phase 7 - Database Integration | Post 11 | Database Integration — moleculer-db with MongoDB and Mongoose

Post 11 of 15 | Phase 7: Database Integration


Database Integration — moleculer-db with MongoDB and Mongoose

In every post so far your services have been using in-memory arrays to store data. Those arrays reset every time you restart the server. In this post we replace them with a real MongoDB database using moleculer-db, Moleculer's official database integration package.

moleculer-db is one of the most useful packages in the Moleculer ecosystem. It gives you free CRUD actions out of the box. You define your model and moleculer-db automatically creates list, find, get, create, update, and remove actions for you without writing a single line of handler code.


What is moleculer-db

moleculer-db is a service mixin, just like moleculer-web. You mix it into your service and it adds database capabilities. It supports multiple database adapters:

  • Memory adapter — built-in, good for testing
  • MongoDB adapter using Mongoose — most common in Node.js projects
  • MongoDB adapter using native driver
  • Sequelize adapter — for SQL databases

In this post we use the Mongoose adapter because you already know Mongoose from your MERN experience.


Installing Required Packages

npm install moleculer-db moleculer-db-adapter-mongoose mongoose

Make sure MongoDB is running on your machine. If you have MongoDB installed locally:

mongod

Or if you use MongoDB Atlas, get your connection string ready.


How moleculer-db Works — The Big Picture

Without moleculer-db, a typical service action looks like this:

// You write all of this yourself
actions: {
    list: {
        async handler(ctx) {
            const users = await User.find({});
            return users;
        }
    },
    getById: {
        async handler(ctx) {
            const user = await User.findById(ctx.params.id);
            return user;
        }
    },
    create: {
        async handler(ctx) {
            const user = new User(ctx.params);
            await user.save();
            return user;
        }
    }
    // ... and so on for update, delete
}

With moleculer-db, you write this instead:

// moleculer-db gives you all CRUD actions for free
mixins: [DbMixin],
model: UserModel,
// That is it. list, get, create, update, remove — all done.

moleculer-db reads your model and generates all standard database operations automatically.


Built-in Actions Provided by moleculer-db

When you mix moleculer-db into a service, you automatically get these actions:

serviceName.find       — Find records with filtering and sorting
serviceName.count      — Count matching records
serviceName.list       — Paginated list with total count
serviceName.create     — Create one record
serviceName.insert     — Insert one or many records
serviceName.get        — Get one record by ID
serviceName.update     — Update one record by ID
serviceName.remove     — Delete one record by ID

All of these are available immediately. You do not write any handler code for them.


Building the User Service With moleculer-db

Create a models folder in your project root and create your first Mongoose model.

models/user.model.js

"use strict";

const mongoose = require("mongoose");

const UserSchema = new mongoose.Schema(
    {
        name: {
            type: String,
            required: true,
            trim: true
        },
        email: {
            type: String,
            required: true,
            unique: true,
            lowercase: true,
            trim: true
        },
        password: {
            type: String,
            required: true,
            minlength: 6
        },
        role: {
            type: String,
            enum: ["admin", "user"],
            default: "user"
        },
        isActive: {
            type: Boolean,
            default: true
        }
    },
    {
        timestamps: true  // Automatically adds createdAt and updatedAt
    }
);

module.exports = mongoose.model("User", UserSchema);

Now rewrite the user service to use moleculer-db:

services/user.service.js

"use strict";

const { MoleculerClientError } = require("moleculer").Errors;
const DbMixin = require("moleculer-db");
const MongooseAdapter = require("moleculer-db-adapter-mongoose");
const jwt = require("jsonwebtoken");
const bcrypt = require("bcryptjs");
const UserModel = require("../models/user.model");

const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";

module.exports = {
    name: "user",

    // Mix in the database mixin
    mixins: [DbMixin],

    // Tell moleculer-db which adapter and model to use
    adapter: new MongooseAdapter(
        process.env.MONGO_URI || "mongodb://localhost:27017/moleculer-course"
    ),

    // The Mongoose model
    model: UserModel,

    // Customize built-in actions
    // You can disable, override, or add options to the free actions
    settings: {
        // Fields to return in responses
        // Fields not in this list are hidden from API responses
        fields: ["_id", "name", "email", "role", "isActive", "createdAt"],

        // Fields that are NOT allowed in create/update calls from outside
        // Password handling is done manually in custom actions
        entityValidator: {
            name: { type: "string", min: 2 },
            email: "email",
            password: { type: "string", min: 6 },
            role: { type: "string", optional: true }
        }
    },

    actions: {
        // Override the built-in create action to hash password
        create: {
            rest: { method: "POST", path: "/" },
            params: {
                name: { type: "string", min: 2 },
                email: "email",
                password: { type: "string", min: 6 },
                role: { type: "string", optional: true }
            },
            async handler(ctx) {
                // Check if email already exists
                const exists = await this.adapter.findOne({
                    email: ctx.params.email
                });

                if (exists) {
                    throw new MoleculerClientError(
                        "Email already registered",
                        422,
                        "EMAIL_EXISTS"
                    );
                }

                // Hash the password before saving
                const hashedPassword = await bcrypt.hash(ctx.params.password, 10);

                // Use this._create() — the internal moleculer-db create method
                // This is different from ctx.call("user.create") which is the public action
                const user = await this.adapter.insert({
                    name: ctx.params.name,
                    email: ctx.params.email,
                    password: hashedPassword,
                    role: ctx.params.role || "user"
                });

                // Return user without password
                return this.sanitizeUser(user);
            }
        },

        // Custom login action — not provided by moleculer-db
        login: {
            rest: { method: "POST", path: "/login" },
            params: {
                email: "email",
                password: "string"
            },
            async handler(ctx) {
                // Find user by email
                const user = await this.adapter.findOne({
                    email: ctx.params.email
                });

                if (!user) {
                    throw new MoleculerClientError(
                        "Invalid email or password",
                        401,
                        "INVALID_CREDENTIALS"
                    );
                }

                // Compare passwords
                const isMatch = await bcrypt.compare(
                    ctx.params.password,
                    user.password
                );

                if (!isMatch) {
                    throw new MoleculerClientError(
                        "Invalid email or password",
                        401,
                        "INVALID_CREDENTIALS"
                    );
                }

                // Generate JWT
                const token = jwt.sign(
                    {
                        id: user._id.toString(),
                        email: user.email,
                        role: user.role
                    },
                    JWT_SECRET,
                    { expiresIn: "24h" }
                );

                // Put token in meta so API Gateway can send it as header
                ctx.meta.token = token;

                return {
                    message: "Login successful",
                    user: this.sanitizeUser(user)
                };
            }
        },

        // Get the currently authenticated user's profile
        me: {
            rest: { method: "GET", path: "/me" },
            async handler(ctx) {
                // ctx.meta.user is set by the API Gateway authenticate hook
                const userId = ctx.meta.user.id;

                const user = await this.adapter.findById(userId);

                if (!user) {
                    throw new MoleculerClientError(
                        "User not found",
                        404,
                        "NOT_FOUND"
                    );
                }

                return this.sanitizeUser(user);
            }
        },

        // Override built-in list to add REST mapping
        list: {
            rest: { method: "GET", path: "/" }
        },

        // Override built-in get to add REST mapping
        get: {
            rest: { method: "GET", path: "/:id" }
        },

        // Override built-in update to add REST mapping
        update: {
            rest: { method: "PUT", path: "/:id" }
        },

        // Override built-in remove to add REST mapping
        remove: {
            rest: { method: "DELETE", path: "/:id" }
        }
    },

    methods: {
        // Private helper — removes password from user object before returning
        sanitizeUser(user) {
            const sanitized = user.toObject ? user.toObject() : { ...user };
            delete sanitized.password;
            delete sanitized.__v;
            return sanitized;
        }
    },

    async started() {
        this.logger.info("User service connected to MongoDB");
    }
};

Install bcryptjs for password hashing:

npm install bcryptjs

Building the Product Service With moleculer-db

models/product.model.js

"use strict";

const mongoose = require("mongoose");

const ProductSchema = new mongoose.Schema(
    {
        name: {
            type: String,
            required: true,
            trim: true
        },
        description: {
            type: String,
            default: ""
        },
        price: {
            type: Number,
            required: true,
            min: 0
        },
        stock: {
            type: Number,
            required: true,
            min: 0,
            default: 0
        },
        category: {
            type: String,
            default: "general"
        },
        isActive: {
            type: Boolean,
            default: true
        }
    },
    {
        timestamps: true
    }
);

module.exports = mongoose.model("Product", ProductSchema);

services/product.service.js

"use strict";

const { MoleculerClientError } = require("moleculer").Errors;
const DbMixin = require("moleculer-db");
const MongooseAdapter = require("moleculer-db-adapter-mongoose");
const ProductModel = require("../models/product.model");

module.exports = {
    name: "product",

    mixins: [DbMixin],

    adapter: new MongooseAdapter(
        process.env.MONGO_URI || "mongodb://localhost:27017/moleculer-course"
    ),

    model: ProductModel,

    settings: {
        fields: ["_id", "name", "description", "price", "stock", "category", "isActive", "createdAt"],

        populates: {}
    },

    actions: {
        // Map REST routes to built-in actions
        list: {
            rest: { method: "GET", path: "/" }
        },
        get: {
            rest: { method: "GET", path: "/:id" }
        },
        create: {
            rest: { method: "POST", path: "/" }
        },
        update: {
            rest: { method: "PUT", path: "/:id" }
        },
        remove: {
            rest: { method: "DELETE", path: "/:id" }
        },

        // Custom action — reduce stock when order is placed
        reduceStock: {
            params: {
                id: "string",
                quantity: "number"
            },
            async handler(ctx) {
                const product = await this.adapter.findById(ctx.params.id);

                if (!product) {
                    throw new MoleculerClientError(
                        "Product not found",
                        404,
                        "NOT_FOUND"
                    );
                }

                if (product.stock < ctx.params.quantity) {
                    throw new MoleculerClientError(
                        "Insufficient stock",
                        422,
                        "INSUFFICIENT_STOCK",
                        { available: product.stock, requested: ctx.params.quantity }
                    );
                }

                // Update stock
                const updated = await this.adapter.updateById(ctx.params.id, {
                    $inc: { stock: -ctx.params.quantity }
                });

                // Clear cache
                await this.broker.broadcast("cache.clean.product");

                return updated;
            }
        }
    },

    events: {
        // When an order is created, reduce the product stock
        async "order.created"(ctx) {
            const order = ctx.params;
            this.logger.info(`Reducing stock for product ${order.productId}`);

            await ctx.call("product.reduceStock", {
                id: order.productId,
                quantity: order.quantity
            });
        },

        "cache.clean.product"() {
            if (this.broker.cacher) {
                this.broker.cacher.clean("product.**");
            }
        }
    }
};

Building the Order Service With moleculer-db

models/order.model.js

"use strict";

const mongoose = require("mongoose");

const OrderSchema = new mongoose.Schema(
    {
        userId: {
            type: mongoose.Schema.Types.ObjectId,
            ref: "User",
            required: true
        },
        productId: {
            type: mongoose.Schema.Types.ObjectId,
            ref: "Product",
            required: true
        },
        quantity: {
            type: Number,
            required: true,
            min: 1
        },
        totalPrice: {
            type: Number,
            required: true
        },
        status: {
            type: String,
            enum: ["pending", "confirmed", "shipped", "delivered", "cancelled"],
            default: "pending"
        }
    },
    {
        timestamps: true
    }
);

module.exports = mongoose.model("Order", OrderSchema);

services/order.service.js

"use strict";

const { MoleculerClientError } = require("moleculer").Errors;
const DbMixin = require("moleculer-db");
const MongooseAdapter = require("moleculer-db-adapter-mongoose");
const OrderModel = require("../models/order.model");

module.exports = {
    name: "order",

    mixins: [DbMixin],

    adapter: new MongooseAdapter(
        process.env.MONGO_URI || "mongodb://localhost:27017/moleculer-course"
    ),

    model: OrderModel,

    settings: {
        fields: ["_id", "userId", "productId", "quantity", "totalPrice", "status", "createdAt"]
    },

    actions: {
        list: {
            rest: { method: "GET", path: "/" }
        },
        get: {
            rest: { method: "GET", path: "/:id" }
        },

        // Override create to add business logic
        create: {
            rest: { method: "POST", path: "/" },
            params: {
                productId: "string",
                quantity: { type: "number", min: 1 }
            },
            async handler(ctx) {
                const userId = ctx.meta.user.id;

                // Get the product to check stock and get price
                const product = await ctx.call("product.get", {
                    id: ctx.params.productId
                });

                if (!product) {
                    throw new MoleculerClientError(
                        "Product not found",
                        404,
                        "NOT_FOUND"
                    );
                }

                if (product.stock < ctx.params.quantity) {
                    throw new MoleculerClientError(
                        "Insufficient stock",
                        422,
                        "INSUFFICIENT_STOCK"
                    );
                }

                // Calculate total price
                const totalPrice = product.price * ctx.params.quantity;

                // Save order to database using adapter directly
                const order = await this.adapter.insert({
                    userId,
                    productId: ctx.params.productId,
                    quantity: ctx.params.quantity,
                    totalPrice,
                    status: "confirmed"
                });

                // Emit event — product service will reduce stock
                // email service will send confirmation
                ctx.emit("order.created", {
                    orderId: order._id.toString(),
                    userId,
                    productId: ctx.params.productId,
                    quantity: ctx.params.quantity,
                    totalPrice,
                    productName: product.name
                });

                return order;
            }
        },

        // Get all orders for the authenticated user
        myOrders: {
            rest: { method: "GET", path: "/my" },
            async handler(ctx) {
                const userId = ctx.meta.user.id;

                const orders = await this.adapter.find({
                    query: { userId }
                });

                return orders;
            }
        },

        // Update order status
        updateStatus: {
            rest: { method: "PUT", path: "/:id/status" },
            params: {
                id: "string",
                status: {
                    type: "enum",
                    values: ["pending", "confirmed", "shipped", "delivered", "cancelled"]
                }
            },
            async handler(ctx) {
                const order = await this.adapter.updateById(ctx.params.id, {
                    $set: { status: ctx.params.status }
                });

                if (!order) {
                    throw new MoleculerClientError(
                        "Order not found",
                        404,
                        "NOT_FOUND"
                    );
                }

                // Emit status change event
                ctx.emit("order.statusUpdated", {
                    orderId: ctx.params.id,
                    status: ctx.params.status
                });

                return order;
            }
        }
    }
};

Useful moleculer-db Adapter Methods

These are the methods you use inside your action handlers when you need custom database operations beyond the built-in actions:

// Find one record matching a query
await this.adapter.findOne({ email: "rahul@example.com" });

// Find by MongoDB ID
await this.adapter.findById("60d5f484b9e5c82f9c5e4d3a");

// Find multiple records
await this.adapter.find({
    query: { isActive: true },      // filter
    sort: ["-createdAt"],           // sort by createdAt descending
    limit: 10,                      // max results
    offset: 0                       // skip
});

// Count records
await this.adapter.count({ query: { isActive: true } });

// Insert one record
await this.adapter.insert({ name: "Rahul", email: "rahul@example.com" });

// Update by ID — MongoDB update operators work here
await this.adapter.updateById("60d5f484b9e5c82f9c5e4d3a", {
    $set: { name: "Updated Name" },
    $inc: { loginCount: 1 }
});

// Delete by ID
await this.adapter.removeById("60d5f484b9e5c82f9c5e4d3a");

Environment Variables — Keeping Config Out of Code

Never hardcode your MongoDB URI or JWT secret in your code. Use environment variables. Create a .env file in your project root:

.env

MONGO_URI=mongodb://localhost:27017/moleculer-course
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
PORT=3000

Install dotenv:

npm install dotenv

Load it at the very top of moleculer.config.js:

"use strict";

require("dotenv").config();

module.exports = {
    namespace: "ecommerce",

    // Now these read from .env file
    // process.env.MONGO_URI is available everywhere
};

Add .env to your .gitignore:

node_modules/
.env

Updated Folder Structure

Your project should now look like this:

my-project/
  models/
    user.model.js
    product.model.js
    order.model.js
  services/
    api.service.js
    user.service.js
    product.service.js
    order.service.js
    email.service.js
    notification.service.js
  moleculer.config.js
  package.json
  .env
  .gitignore

Testing the Complete Flow

Start your project:

npm run dev

Step 1: Create a product

POST http://localhost:3000/api/product
Body: {
    "name": "Mechanical Keyboard",
    "description": "RGB mechanical keyboard with Cherry MX switches",
    "price": 2999,
    "stock": 50,
    "category": "electronics"
}

Step 2: Register a user

POST http://localhost:3000/api/public/register
Body: {
    "name": "Rahul Sharma",
    "email": "rahul@example.com",
    "password": "password123"
}

Step 3: Login

POST http://localhost:3000/api/public/login
Body: {
    "email": "rahul@example.com",
    "password": "password123"
}

Copy the token from the Authorization response header.

Step 4: Place an order

POST http://localhost:3000/api/order
Headers: Authorization: Bearer <token>
Body: {
    "productId": "<product id from step 1>",
    "quantity": 2
}

Step 5: Check your orders

GET http://localhost:3000/api/order/my
Headers: Authorization: Bearer <token>

Step 6: Check product stock reduced

GET http://localhost:3000/api/product/<product id>

Stock should now show 48 instead of 50.

Your entire backend is now backed by MongoDB with real data persistence.


Summary

  • moleculer-db is a service mixin that adds database capabilities to any service.
  • It provides free CRUD actions: list, find, count, get, create, update, remove, insert.
  • Use MongooseAdapter to connect to MongoDB with Mongoose models.
  • Define your Mongoose model in a separate models folder and pass it to the service.
  • Override built-in actions when you need custom business logic like password hashing.
  • Use this.adapter methods directly inside custom action handlers for database operations.
  • The settings.fields array controls which fields are returned in API responses.
  • Always hash passwords with bcrypt before saving to database.
  • Always put sensitive config like MONGO_URI and JWT_SECRET in .env file.
  • Never commit .env to git.

Up Next

Post 12 covers Logging, Metrics, and Tracing — the three pillars of observability. You will learn how to set up structured logging, expose Prometheus metrics, and trace requests across services using Jaeger. This is what allows you to understand what is happening inside your running application.


Course Progress: 11 of 15 posts complete.

No comments:

Post a Comment

Phase 9 - Final Post | Post 15 | What to Learn Next — Testing, Advanced Patterns, Kubernetes, and the Road Ahead

Post 15 of 15 | Final Post What to Learn Next — Testing, Advanced Patterns, Kubernetes, and the Road Ahead You have completed the core Mo...