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