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 Moleculer course. You can build a full microservices backend, containerize it, add fault tolerance, caching, authentication, observability, and deploy it with Docker Compose. That puts you ahead of most Node.js developers.

This final post covers what comes after. The topics here are not small additions. Each one is a significant skill area. Think of this post as your roadmap for the next six to twelve months.


Topic 1: Testing Moleculer Services

Testing is the first thing you should learn after completing this course. Untested microservices are dangerous because failures in one service cascade into others in unexpected ways.

Moleculer has excellent testing support built in.

Unit Testing Actions

For unit testing individual actions, you do not need a running broker. You test the handler function in isolation.

Install Jest:

npm install --save-dev jest

Create a test file:

tests/unit/user.service.test.js

"use strict";

const { ServiceBroker } = require("moleculer");
const UserService = require("../../services/user.service");

describe("User Service", () => {
    let broker;

    // Create a broker before all tests
    beforeAll(async () => {
        broker = new ServiceBroker({ logger: false });
        broker.createService(UserService);
        await broker.start();
    });

    // Stop broker after all tests
    afterAll(async () => {
        await broker.stop();
    });

    describe("user.login action", () => {
        it("should return a token on valid credentials", async () => {
            const result = await broker.call("user.login", {
                email: "rahul@example.com",
                password: "password123"
            });

            expect(result).toHaveProperty("message", "Login successful");
            expect(result).toHaveProperty("user");
            expect(result.user).not.toHaveProperty("password");
        });

        it("should throw error on invalid credentials", async () => {
            await expect(
                broker.call("user.login", {
                    email: "wrong@example.com",
                    password: "wrongpassword"
                })
            ).rejects.toThrow("Invalid email or password");
        });

        it("should throw validation error when email is missing", async () => {
            await expect(
                broker.call("user.login", {
                    password: "password123"
                })
            ).rejects.toThrow();
        });
    });
});

Integration Testing — Multiple Services

Integration tests test how services communicate with each other:

"use strict";

const { ServiceBroker } = require("moleculer");
const UserService = require("../../services/user.service");
const OrderService = require("../../services/order.service");
const ProductService = require("../../services/product.service");

describe("Order Flow Integration", () => {
    let broker;

    beforeAll(async () => {
        broker = new ServiceBroker({ logger: false });

        // Load all services into one broker for integration testing
        broker.createService(UserService);
        broker.createService(OrderService);
        broker.createService(ProductService);

        await broker.start();
    });

    afterAll(async () => {
        await broker.stop();
    });

    it("should create an order and reduce product stock", async () => {
        // Step 1: Create a product
        const product = await broker.call("product.create", {
            name: "Test Keyboard",
            price: 1000,
            stock: 10
        });

        // Step 2: Create an order
        const order = await broker.call("order.create", {
            productId: product._id.toString(),
            quantity: 2
        }, {
            meta: { user: { id: "user-1", role: "user" } }
        });

        expect(order).toHaveProperty("status", "confirmed");
        expect(order).toHaveProperty("totalPrice", 2000);

        // Step 3: Verify stock reduced
        const updatedProduct = await broker.call("product.get", {
            id: product._id.toString()
        });

        expect(updatedProduct.stock).toBe(8);
    });
});

Mocking Services in Tests

When you want to test one service in isolation without its dependencies, mock the dependency:

describe("Order Service with mocked User Service", () => {
    let broker;

    beforeAll(async () => {
        broker = new ServiceBroker({ logger: false });

        // Mock the user service — return fake data
        broker.createService({
            name: "user",
            actions: {
                get() {
                    return { id: "1", name: "Test User", email: "test@test.com" };
                }
            }
        });

        broker.createService(OrderService);
        await broker.start();
    });

    afterAll(() => broker.stop());

    it("should create order using mocked user", async () => {
        const order = await broker.call("order.create", {
            productId: "product-1",
            quantity: 1
        }, {
            meta: { user: { id: "1" } }
        });

        expect(order).toBeDefined();
    });
});

Add test script to package.json:

"scripts": {
    "test": "jest --testEnvironment=node",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
}

Topic 2: The Saga Pattern — Distributed Transactions

This is one of the most important advanced topics in microservices. Understand the problem first.

In a monolith, if you create an order and charge a credit card in the same function, and the charge fails, you can roll back the entire database transaction. Both operations either succeed or both fail. This is called an ACID transaction.

In microservices, the order is saved in the order service database and the payment is charged in the payment service database. They are separate processes, separate databases. If the payment fails after the order was saved, there is no global rollback. You now have an order in the database with no corresponding payment. Data is inconsistent.

The Saga pattern solves this. A saga is a sequence of local transactions. If any step fails, the saga runs compensating transactions to undo the previous steps.

There are two types of sagas:

Choreography-based Saga — Services communicate through events. No central coordinator. Each service knows what to do when it receives an event and what to do if something goes wrong.

order.service     → emits "order.created"
payment.service   → listens, charges card, emits "payment.completed" or "payment.failed"
order.service     → listens to "payment.completed" → updates order to "confirmed"
order.service     → listens to "payment.failed"    → updates order to "cancelled"
inventory.service → listens to "order.confirmed"   → reduces stock

Orchestration-based Saga — A central saga orchestrator calls each step and handles failures:

// saga/order.saga.js
module.exports = {
    name: "order-saga",

    actions: {
        async start(ctx) {
            const { userId, productId, quantity } = ctx.params;

            let orderId = null;

            try {
                // Step 1: Create order
                const order = await ctx.call("order.create", {
                    userId, productId, quantity
                });
                orderId = order._id;

                // Step 2: Charge payment
                await ctx.call("payment.charge", {
                    userId,
                    amount: order.totalPrice,
                    orderId
                });

                // Step 3: Reduce stock
                await ctx.call("product.reduceStock", {
                    id: productId,
                    quantity
                });

                // All steps succeeded
                await ctx.call("order.updateStatus", {
                    id: orderId,
                    status: "confirmed"
                });

                return { success: true, orderId };

            } catch (err) {
                this.logger.error("Saga failed, running compensation", err);

                // Compensating transaction — undo what was done
                if (orderId) {
                    await ctx.call("order.updateStatus", {
                        id: orderId,
                        status: "cancelled"
                    });

                    // If payment was charged, refund it
                    await ctx.call("payment.refund", {
                        orderId
                    }).catch(e => this.logger.error("Refund failed", e));
                }

                throw err;
            }
        }
    }
};

Moleculer has an official package called moleculer-workflows that provides a more structured saga implementation. Look it up at github.com/moleculerjs/moleculer-workflows when you are ready for this.


Topic 3: Advanced moleculer-db — Populates and Hooks

You used basic moleculer-db in Post 11. Two features you will need in real projects:

Populates — Joining Data Across Services

Instead of manually calling user service from order service to get user details, moleculer-db can auto-populate related data:

module.exports = {
    name: "order",
    mixins: [DbMixin],
    model: OrderModel,

    settings: {
        populates: {
            // When listing orders, automatically fetch user details
            user: {
                action: "user.get",
                params: {
                    fields: ["name", "email"]
                }
            },

            // Automatically fetch product details
            product: {
                action: "product.get",
                params: {
                    fields: ["name", "price"]
                }
            }
        }
    },

    actions: {
        list: {
            rest: { method: "GET", path: "/" },
            params: {
                // Client can request population
                populate: { type: "array", items: "string", optional: true }
            }
        }
    }
};

Call with populate:

GET /api/order?populate[]=user&populate[]=product

Response:

{
    "rows": [
        {
            "_id": "order-1",
            "userId": "user-1",
            "user": { "name": "Rahul Sharma", "email": "rahul@example.com" },
            "productId": "product-1",
            "product": { "name": "Mechanical Keyboard", "price": 2999 },
            "quantity": 2,
            "totalPrice": 5998
        }
    ]
}

Entity Hooks — Before and After Database Operations

module.exports = {
    name: "product",
    mixins: [DbMixin],

    hooks: {
        before: {
            // Runs before every create action
            create(ctx) {
                // Normalize data before saving
                ctx.params.name = ctx.params.name.trim();
                ctx.params.slug = ctx.params.name.toLowerCase().replace(/\s+/g, "-");
            }
        },
        after: {
            // Runs after every get action
            get(ctx, res) {
                // Transform data before returning
                if (res) {
                    res.priceFormatted = `Rs. ${res.price.toLocaleString()}`;
                }
                return res;
            }
        }
    }
};

Topic 4: Moleculer Middlewares

Middlewares in Moleculer are different from Express middlewares. They intercept action calls, event emissions, and broker lifecycle events globally. They are the correct way to add cross-cutting behavior like logging every action call, adding rate limiting, or transforming responses globally.

// middlewares/logger.middleware.js
module.exports = {
    name: "ActionLogger",

    // Wraps every action call
    localAction(next, action) {
        return async function(ctx) {
            const start = Date.now();

            try {
                const result = await next(ctx);
                const duration = Date.now() - start;

                ctx.broker.logger.info(`Action ${action.name} completed in ${duration}ms`);

                return result;
            } catch (err) {
                const duration = Date.now() - start;
                ctx.broker.logger.error(`Action ${action.name} failed after ${duration}ms`, err.message);
                throw err;
            }
        };
    }
};

Register in moleculer.config.js:

const ActionLogger = require("./middlewares/logger.middleware");

module.exports = {
    middlewares: [ActionLogger]
};

Now every action call in every service is automatically logged with duration. Zero changes to service code.


Topic 5: Kubernetes — Beyond Docker Compose

Docker Compose is excellent for development and small production deployments. For large scale production, Kubernetes is the standard.

Kubernetes gives you:

  • Automatic restart of crashed containers
  • Rolling deployments with zero downtime
  • Auto-scaling based on CPU or request count
  • Load balancing across pods
  • Health checks and readiness probes
  • Secrets management

A basic Kubernetes deployment for your user service:

k8s/user-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
spec:
  replicas: 3              # Run 3 instances
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
    spec:
      containers:
        - name: user-service
          image: your-registry/ecommerce:latest
          env:
            - name: SERVICES
              value: "user"
            - name: MONGO_URI
              valueFrom:
                secretKeyRef:
                  name: app-secrets
                  key: mongo-uri
            - name: REDIS_URI
              valueFrom:
                secretKeyRef:
                  name: app-secrets
                  key: redis-uri
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
            limits:
              memory: "256Mi"
              cpu: "500m"
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 30
            periodSeconds: 10

The path from Docker Compose to Kubernetes is well documented. When you are ready, look at:

  • Minikube for running Kubernetes locally
  • Helm for packaging Kubernetes applications
  • Google Kubernetes Engine or AWS EKS for managed Kubernetes in the cloud

Topic 6: Additional Official Moleculer Packages

There are many official and community packages that extend Moleculer. These are the most useful ones for real projects:

moleculer-mail — Send emails via various providers (Mailgun, SendGrid, SMTP):

npm install moleculer-mail

moleculer-bull — Background job queues using Bull and Redis. For long-running tasks that should not block the request:

npm install moleculer-bull

moleculer-cron — Scheduled jobs using cron expressions:

npm install moleculer-cron

moleculer-io — Socket.IO integration for real-time features:

npm install moleculer-io

moleculer-apollo-server — GraphQL support alongside REST:

npm install moleculer-apollo-server

moleculer-telegram — Send Telegram messages from services. Useful for production alerts:

npm install moleculer-telegram

Topic 7: TypeScript with Moleculer

Moleculer has first-class TypeScript support. If your team uses TypeScript, you can write strongly typed services:

npm install typescript @types/node
npm install --save-dev ts-node

A TypeScript service looks like this:

import { Service, ServiceSchema, Context } from "moleculer";

interface UserCreateParams {
    name: string;
    email: string;
    password: string;
}

interface UserResponse {
    id: string;
    name: string;
    email: string;
}

const UserService: ServiceSchema = {
    name: "user",

    actions: {
        create: {
            params: {
                name: "string",
                email: "email",
                password: { type: "string", min: 6 }
            },
            async handler(ctx: Context<UserCreateParams>): Promise<UserResponse> {
                const { name, email, password } = ctx.params;
                // TypeScript now knows exactly what fields are available
                return { id: "1", name, email };
            }
        }
    }
};

export default UserService;

TypeScript catches errors at compile time instead of runtime. For large teams and long-lived projects, this is extremely valuable.


Where to Find Help

When you are stuck, these are the best places to get answers:

Official Documentation

moleculer.services/docs/0.15 — Always read the official docs first. They are well written and kept up to date.

GitHub Repository

github.com/moleculerjs/moleculer — Read the source code, check existing issues, open new issues.

Discord Community

discord.gg/TSEcDRP — The Moleculer Discord has an active community. The maintainers themselves answer questions there.

Stack Overflow

Search for questions tagged with moleculer. Many common problems are already answered.

GitHub Discussions

github.com/moleculerjs/moleculer/discussions — For longer questions and architectural discussions.


Recommended Learning Path After This Course

Here is a structured path for the next six to twelve months based on what you have learned:

Month 1 and 2: Solidify what you learned

  • Add tests to the project you built in this course
  • Add the saga pattern to your order flow
  • Deploy your project to a real server using Docker Compose
  • Get comfortable with Jaeger traces and Prometheus metrics

Month 3 and 4: Go deeper into the ecosystem

  • Learn moleculer-bull for background jobs
  • Add GraphQL support with moleculer-apollo-server
  • Add WebSocket support with moleculer-io
  • Practice the populates feature of moleculer-db

Month 5 and 6: Production skills

  • Learn Kubernetes basics with Minikube
  • Convert your Docker Compose deployment to Kubernetes
  • Set up a proper CI/CD pipeline with GitHub Actions
  • Learn about service mesh concepts with Istio

Month 7 and beyond: Architecture

  • Study the twelve-factor app methodology at 12factor.net
  • Read about domain-driven design — it helps you decide how to split services
  • Study event sourcing and CQRS patterns
  • Look at other microservices frameworks to understand tradeoffs — NestJS microservices, Seneca

Final Thoughts

When you started this course you knew MERN and MEAN. You could build a monolith. You did not know how to think in services or how to handle the complexity that comes with distributed systems.

Now you understand:

  • Why microservices exist and when they are the right choice
  • How to design services with clear boundaries
  • How to make services communicate reliably
  • How to protect your system when things fail
  • How to observe what is happening inside your running application
  • How to package and deploy a complete microservices system

One important thing to remember as you go forward: microservices are not always the right choice. For a small team, a small application, or an early startup, a well-structured monolith is often better. Start with a monolith, identify the boundaries, and extract services when you have a real reason — a team needs to work independently, a specific component needs to scale differently, or a part needs a different technology.

Moleculer is powerful but it is a tool. Use it when it solves a real problem. The knowledge you have gained about distributed systems, fault tolerance, event-driven architecture, and observability applies far beyond Moleculer. These concepts work in any language, any framework, any cloud.

You are now equipped to build production-grade distributed systems with Node.js. That is a significant achievement.


Complete Course Summary — All 15 Posts

Phase 1 — Foundation
  Post 1:  What is microservices and why Moleculer
  Post 2:  Installation and first working service

Phase 2 — Core Concepts
  Post 3:  ServiceBroker and moleculer.config.js
  Post 4:  Services, Actions, validation, error handling
  Post 5:  Events, emit vs broadcast, wildcard listeners

Phase 3 — Communication
  Post 6:  Context object, requestID, meta, call chain
  Post 7:  Transporters, TCP, NATS, Redis, service discovery

Phase 4 — Fault Tolerance
  Post 8:  Timeout, Retry, Circuit Breaker, Bulkhead, Fallback

Phase 5 — Caching
  Post 9:  Memory and Redis caching, TTL, cache invalidation

Phase 6 — API Gateway
  Post 10: moleculer-web, authentication, authorization, JWT

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

Phase 8 — Observability
  Post 12: Logging, Prometheus metrics, Jaeger tracing

Phase 9 — Deployment
  Post 13: Docker and Docker Compose
  Post 14: Complete project review and best practices
  Post 15: Testing, Sagas, Kubernetes, what to learn next

Course Progress: 15 of 15 posts complete.

You have completed the Moleculer Microservices Course.

Phase 9 - Final Project Review and Best Practices | Post 14 | Putting It All Together — Complete E-Commerce Project Review and Best Practices

Post 14 of 15 | Phase 9: Final Project Review and Best Practices


Putting It All Together — Complete E-Commerce Project Review and Best Practices

You have come a long way. In the previous thirteen posts you learned every major concept in Moleculer from scratch. This post ties everything together. We will do a full review of the complete project, fill in the gaps, establish best practices for real production code, and look at common mistakes that developers make when moving from development to production.


What You Have Built

Let us take a clear inventory of what exists in your project right now:

Infrastructure:
  MongoDB       — primary database for all services
  Redis         — transporter between services and shared cache
  Jaeger        — distributed tracing and request visualization
  Docker        — containerization of every component

Services:
  api           — HTTP entry point, authentication, routing
  user          — registration, login, profile management
  product       — product catalog, stock management
  order         — order creation, status management
  email         — reacts to events, sends emails
  notification  — reacts to events, sends push notifications

Patterns Used:
  Request-Reply — actions between services
  Event-Driven  — order.created triggers email and notification
  Caching       — product and user data cached in Redis
  Fault Tolerance — circuit breaker, retry, timeout, bulkhead
  Authentication — JWT verified in API Gateway, user in ctx.meta
  Tracing        — full request chain visible in Jaeger

This is a production-grade microservices architecture. Not a toy project. Real companies run architectures exactly like this.


Completing the Email Service

In earlier posts the email service just printed log messages. Let us make it real using nodemailer:

npm install nodemailer

services/email.service.js

"use strict";

const nodemailer = require("nodemailer");

module.exports = {
    name: "email",

    settings: {
        // In production use real SMTP credentials from environment
        smtp: {
            host: process.env.SMTP_HOST || "smtp.ethereal.email",
            port: process.env.SMTP_PORT || 587,
            secure: false,
            auth: {
                user: process.env.SMTP_USER || "",
                pass: process.env.SMTP_PASS || ""
            }
        },
        from: process.env.SMTP_FROM || "noreply@ecommerce.com"
    },

    created() {
        // Create nodemailer transporter when service starts
        this.transporter = nodemailer.createTransport(this.settings.smtp);
    },

    actions: {
        // Can also be called directly as an action
        send: {
            params: {
                to: "email",
                subject: "string",
                html: "string"
            },
            async handler(ctx) {
                await this.sendEmail(
                    ctx.params.to,
                    ctx.params.subject,
                    ctx.params.html
                );
                return { sent: true };
            }
        }
    },

    events: {
        // User registered
        "user.registered": {
            async handler(ctx) {
                const { name, email } = ctx.params;
                await this.sendEmail(
                    email,
                    "Welcome to our store",
                    `<h1>Hi ${name}</h1><p>Thank you for registering. Start shopping now.</p>`
                );
            }
        },

        // Order placed
        "order.created": {
            async handler(ctx) {
                const { userId, productName, totalPrice, orderId } = ctx.params;
                this.logger.info(`Sending order confirmation for order ${orderId}`);

                // In real project, get user email from user service
                // For now we just log
                this.logger.info(`Email: Order ${orderId} confirmed. Total: ${totalPrice}`);
            }
        },

        // Order status changed
        "order.statusUpdated": {
            async handler(ctx) {
                const { orderId, status } = ctx.params;
                this.logger.info(`Email: Order ${orderId} status updated to ${status}`);
            }
        }
    },

    methods: {
        async sendEmail(to, subject, html) {
            try {
                const info = await this.transporter.sendMail({
                    from: this.settings.from,
                    to,
                    subject,
                    html
                });
                this.logger.info(`Email sent to ${to}: ${info.messageId}`);
                return info;
            } catch (err) {
                this.logger.error(`Failed to send email to ${to}`, err);
                // Do not rethrow — email failure should not crash the caller
            }
        }
    }
};

For testing emails locally without a real SMTP server, use Ethereal. Go to ethereal.email, click Create Account, and you get free test SMTP credentials. Emails sent through Ethereal are captured and visible in the browser. No real emails are sent.


Completing the User Service — Emitting Events

Update your user service to emit events when users register and login:

// Inside user service create action, after successful user creation
const newUser = await this.adapter.insert({ ... });

// Emit event for email service to send welcome email
ctx.emit("user.registered", {
    userId: newUser._id.toString(),
    name: newUser.name,
    email: newUser.email
});

return this.sanitizeUser(newUser);

Service Communication Patterns — A Complete Reference

By now you have used many communication patterns. Here they all are in one place:

Pattern 1: Direct action call — synchronous request-reply

Use when you need a result before continuing.

const user = await ctx.call("user.get", { id: "123" });
console.log(user.name); // You have the result

Pattern 2: Parallel action calls — multiple results at once

Use when two calls are independent of each other.

const [user, product] = await Promise.all([
    ctx.call("user.get", { id: userId }),
    ctx.call("product.get", { id: productId })
]);

Pattern 3: Event emit — fire and forget

Use when you want to announce something happened without waiting for reactions.

ctx.emit("order.created", { orderId, userId, total });
// Continue immediately

Pattern 4: Event broadcast — notify all instances

Use when every node needs to know, like cache clearing.

await ctx.broadcast("cache.clean.product");

Pattern 5: Chained calls — sequential with shared context

Each call passes ctx forward maintaining requestID and meta.

const product = await ctx.call("product.get", { id: productId });
const stock = await ctx.call("inventory.check", { productId });
const order = await ctx.call("order.create", { productId, userId });

Best Practices — Service Design

1. One service, one responsibility

A service should have one clear domain. If you cannot describe what a service does in one sentence, it is doing too much. Split it.

Bad: user-order-product.service.js does users, orders, and products. Good: user.service.js, order.service.js, product.service.js each do one thing.

2. Services should not share databases

Each service should own its own data. If user service and order service both directly query the same MongoDB collection, they are tightly coupled. Instead, order service calls user service to get user data.

// Wrong — order service directly queries user collection
const user = await UserModel.findById(userId); // Never do this in order service

// Correct — order service asks user service
const user = await ctx.call("user.get", { id: userId });

3. Always validate input at the action level

Never trust incoming data. Always define params schema on every action.

// Wrong — no validation
actions: {
    create(ctx) {
        return this.adapter.insert(ctx.params); // Dangerous
    }
}

// Correct — explicit validation
actions: {
    create: {
        params: {
            name: { type: "string", min: 2, max: 100 },
            email: "email",
            price: { type: "number", min: 0 }
        },
        handler(ctx) {
            return this.adapter.insert(ctx.params);
        }
    }
}

4. Use methods for reusable logic

Anything used in more than one action belongs in methods.

methods: {
    sanitizeUser(user) {
        const obj = user.toObject ? user.toObject() : { ...user };
        delete obj.password;
        delete obj.__v;
        return obj;
    },

    generateToken(user) {
        return jwt.sign({ id: user._id, role: user.role }, JWT_SECRET, { expiresIn: "24h" });
    }
}

5. Never put business logic in the API Gateway

The API Gateway should only handle HTTP concerns — auth, routing, CORS, rate limiting. All business logic belongs in the appropriate service.

// Wrong — business logic in API Gateway
onBeforeCall(ctx, route, req, res) {
    const user = await validateUser(req.params.userId); // No
    ctx.meta.user = user;
}

// Correct — API Gateway only verifies the token
async authenticate(ctx, route, req) {
    const token = req.headers["authorization"]?.split(" ")[1];
    const decoded = jwt.verify(token, JWT_SECRET);
    return decoded; // Just verify and return, nothing more
}

Best Practices — Error Handling

Use Moleculer error classes consistently:

const { MoleculerClientError, MoleculerServerError } = require("moleculer").Errors;

// Client errors — bad input, not found, unauthorized
// These should NOT be retried
throw new MoleculerClientError("User not found", 404, "NOT_FOUND");
throw new MoleculerClientError("Email already exists", 422, "EMAIL_EXISTS");
throw new MoleculerClientError("Unauthorized", 401, "UNAUTHORIZED");

// Server errors — internal failures worth retrying
// Mark as retryable so retry policy kicks in
const err = new MoleculerServerError("Database timeout", 503, "DB_TIMEOUT");
err.retryable = true;
throw err;

Always handle errors in event handlers:

events: {
    "order.created": {
        async handler(ctx) {
            try {
                await this.sendConfirmationEmail(ctx.params);
            } catch (err) {
                // Log but do not rethrow
                // Event handler errors should never affect the emitter
                this.logger.error("Failed to send order email", err);
            }
        }
    }
}

Add a global error handler in the API Gateway:

settings: {
    onError(req, res, err) {
        const code = err.code || err.status || 500;
        const response = {
            success: false,
            error: {
                message: err.message,
                code: err.type || "INTERNAL_ERROR"
            }
        };

        // Do not expose internal error details in production
        if (process.env.NODE_ENV === "development") {
            response.error.stack = err.stack;
        }

        res.setHeader("Content-Type", "application/json");
        res.writeHead(code);
        res.end(JSON.stringify(response));
    }
}

Best Practices — Security

1. Never expose internal services directly

Only the API Gateway should have a port mapped in docker-compose.yml. All other services communicate over the internal Docker network.

# Correct
api:
  ports:
    - "3000:3000"   # Only this service

user:
  # No ports mapping — internal only
  networks:
    - moleculer-net

2. Use environment variables for all secrets

// Wrong
const JWT_SECRET = "hardcoded-secret-never-do-this";

// Correct
const JWT_SECRET = process.env.JWT_SECRET;
if (!JWT_SECRET) {
    throw new Error("JWT_SECRET environment variable is required");
}

3. Whitelist specific actions in the API Gateway

Do not use wildcard in production routes. Be explicit:

routes: [
    {
        path: "/api",
        whitelist: [
            "user.me",
            "user.update",
            "product.list",
            "product.get",
            "order.create",
            "order.myOrders",
            "order.get"
        ]
        // Admin actions are NOT in this list
    },
    {
        path: "/admin",
        whitelist: [
            "user.list",
            "user.remove",
            "product.create",
            "product.update",
            "product.remove",
            "order.list",
            "order.updateStatus"
        ],
        async authorize(ctx, route, req) {
            if (ctx.meta.user.role !== "admin") {
                throw new Error("Admin access required");
            }
        }
    }
]

4. Sanitize data before returning

Never return sensitive fields like passwords from actions:

methods: {
    sanitizeUser(user) {
        const obj = user.toObject ? user.toObject() : { ...user };
        delete obj.password;
        delete obj.__v;
        delete obj.__id;
        return obj;
    }
}

Best Practices — Performance

1. Cache aggressively but invalidate correctly

Cache reads. Never cache writes. Always invalidate when data changes.

// READ — cache for 5 minutes
getById: {
    cache: { keys: ["id"], ttl: 300 },
    handler(ctx) { ... }
}

// WRITE — always invalidate after
update: {
    async handler(ctx) {
        const result = await this.adapter.updateById(ctx.params.id, ctx.params);
        await this.broker.broadcast("cache.clean.product");
        return result;
    }
}

2. Run independent calls in parallel

// Slow — sequential, 300ms total if each takes 100ms
const user = await ctx.call("user.get", { id: userId });
const product = await ctx.call("product.get", { id: productId });
const address = await ctx.call("address.get", { id: addressId });

// Fast — parallel, 100ms total
const [user, product, address] = await Promise.all([
    ctx.call("user.get", { id: userId }),
    ctx.call("product.get", { id: productId }),
    ctx.call("address.get", { id: addressId })
]);

3. Add MongoDB indexes for frequently queried fields

// In your Mongoose schema
const OrderSchema = new mongoose.Schema({
    userId: { type: mongoose.Schema.Types.ObjectId, ref: "User", index: true },
    status: { type: String, index: true },
    createdAt: { type: Date, index: true }
});

// Compound index for common query patterns
OrderSchema.index({ userId: 1, createdAt: -1 });

4. Set appropriate timeouts per action type

// Fast actions — short timeout
const user = await ctx.call("user.get", { id }, { timeout: 3000 });

// Slow actions — longer timeout
const report = await ctx.call("report.generate", { month: "2026-05" }, { timeout: 60000 });

Common Mistakes to Avoid

Mistake 1: Using broker.call() inside service actions

Always use ctx.call() to preserve the call chain for tracing and metadata propagation.

Mistake 2: Not setting a namespace

Without a namespace, your app's messages mix with any other Moleculer app using the same transporter. Always set namespace in moleculer.config.js.

Mistake 3: Forgetting to await broker.stop()

When writing scripts or tests, always stop the broker cleanly:

broker.start()
    .then(async () => {
        const result = await broker.call("service.action", {});
        console.log(result);
    })
    .finally(() => broker.stop());

Mistake 4: Catching errors silently

// Wrong — swallows the error, debugging nightmare
try {
    const result = await ctx.call("user.get", { id });
} catch (err) {
    // Empty catch — you will never know this failed
}

// Correct — at minimum log it
try {
    const result = await ctx.call("user.get", { id });
} catch (err) {
    this.logger.error("Failed to get user", { id, error: err.message });
    throw err; // Rethrow unless you have a deliberate fallback
}

Mistake 5: Hardcoding service names

If you rename a service, all callers break. Use constants:

// constants/services.js
module.exports = {
    USER: "user",
    ORDER: "order",
    PRODUCT: "product"
};

// In your service
const { USER } = require("../constants/services");
const user = await ctx.call(`${USER}.get`, { id });

Mistake 6: Not using dependencies

If order service starts before user service is ready and immediately tries to call user, it will fail. Always declare dependencies:

module.exports = {
    name: "order",
    dependencies: ["user", "product"],
    // order service will not start until user and product are ready
};

Project Checklist Before Going to Production

Go through this list before deploying:

Authentication and Security:

  • JWT_SECRET is a strong random string from environment variable
  • .env is in .gitignore and never committed to git
  • Only API Gateway is publicly exposed
  • Whitelist is explicit, no wildcard in production routes
  • Passwords are hashed with bcrypt before saving
  • All actions have params validation

Performance:

  • Frequently read actions have caching enabled
  • Cache is invalidated on writes
  • MongoDB indexes exist on queried fields
  • Independent calls use Promise.all

Reliability:

  • Circuit breaker is enabled in moleculer.config.js
  • Retry policy is configured
  • requestTimeout is set
  • Services declare dependencies
  • Event handlers catch their own errors

Observability:

  • Logger format is json in production
  • Metrics are enabled and accessible to Prometheus
  • Tracing is enabled and Jaeger is running
  • requestID is logged with every significant operation

Docker:

  • .dockerignore excludes node_modules and .env
  • All environment variables are in docker-compose.yml not hardcoded
  • Volumes are defined for MongoDB and Redis data
  • Services use container names as hostnames not localhost
  • Only necessary ports are exposed publicly

Complete API Reference for Your Project

Here is a summary of all available endpoints after completing this course:

Public endpoints — no token required:

POST /api/public/register    Register new user
POST /api/public/login       Login and receive JWT token
GET  /api/product            List all products
GET  /api/product/:id        Get one product

Protected endpoints — JWT token required in Authorization header:

GET  /api/user/me            Get authenticated user profile
PUT  /api/user/:id           Update user

POST /api/order              Create new order
GET  /api/order/my           Get authenticated user orders
GET  /api/order/:id          Get one order

Admin endpoints — JWT token with admin role required:

GET    /api/admin/user              List all users
DELETE /api/admin/user/:id          Delete user
POST   /api/admin/product           Create product
PUT    /api/admin/product/:id       Update product
DELETE /api/admin/product/:id       Delete product
GET    /api/admin/order             List all orders
PUT    /api/admin/order/:id/status  Update order status

Summary of the Entire Course

This is everything you learned across all fourteen posts:

Post 1: What microservices are and why Moleculer exists. Post 2: Installation, project setup, first working service. Post 3: ServiceBroker in depth, moleculer.config.js, lifecycle hooks. Post 4: Services and Actions, validation, ctx.meta, error handling. Post 5: Events, fire and forget, emit vs broadcast, wildcard listeners. Post 6: Context object, requestID, caller, call chain tracing. Post 7: Transporters, TCP, NATS, Redis, service discovery, scaling. Post 8: Fault tolerance, timeout, retry, circuit breaker, bulkhead, fallback. Post 9: Caching, memory vs Redis, TTL, cache invalidation with events. Post 10: API Gateway, authentication, authorization, JWT flow, whitelist. Post 11: moleculer-db with MongoDB, free CRUD actions, custom actions, password hashing. Post 12: Logging, Metrics with Prometheus, Tracing with Jaeger. Post 13: Docker, Docker Compose, containerizing all services, Redis transporter. Post 14: Complete project review, best practices, security, performance, production checklist.


Up Next

Post 15 is the final post of this course. It covers what to learn next — testing Moleculer services, advanced patterns like sagas for distributed transactions, service mesh concepts, Kubernetes deployment, and the broader microservices ecosystem. It also covers where to find help, how to contribute to Moleculer, and recommended resources for going deeper.


Course Progress: 14 of 15 posts complete. One post remaining.

Phase 9 - Deployment | Post 13 | Docker and Docker Compose — Running Your Entire Application With One Command

Post 13 of 15 | Phase 9: Deployment


Docker and Docker Compose — Running Your Entire Application With One Command

In every post so far you have been running everything on your local machine manually. MongoDB running separately, Node.js process running with npm run dev, maybe NATS or Redis running in another terminal. In production and even in team development, this manual setup is not sustainable.

Docker solves this by packaging each service into a self-contained unit called a container. Docker Compose then lets you define your entire application — all services, databases, message brokers — in one file and start everything with a single command.

By the end of this post you will be able to run your entire microservices application with:

docker-compose up

And tear it all down with:

docker-compose down

What is Docker — The Core Idea

Think of Docker as a shipping container for software. Before shipping containers existed, loading cargo onto a ship was chaos. Every item had different shapes, sizes, and handling requirements. Shipping containers standardized everything. Any container fits on any ship, any truck, any crane.

Docker does the same for software. Instead of worrying about what operating system the server runs, what version of Node.js is installed, what environment variables are set — you package your application and everything it needs into a container image. That image runs the same way on your laptop, your teammate's laptop, and your production server.


Key Docker Concepts

Dockerfile — A text file with instructions for building a container image. Like a recipe.

Image — The built result of a Dockerfile. A blueprint. Not running yet.

Container — A running instance of an image. The actual running application.

Docker Compose — A tool for defining and running multiple containers together. Uses a docker-compose.yml file.

Volume — A way to persist data outside the container. Without volumes, data is lost when a container stops.

Network — Containers on the same Docker network can communicate with each other by service name.


Installing Docker

Go to docker.com/get-started and download Docker Desktop for Windows. Install it and start it. Verify installation:

docker --version
docker-compose --version

You should see version numbers for both.


Writing the Dockerfile for Your Services

Every Node.js service needs a Dockerfile. Since all your services are in one project, you write one Dockerfile at the project root that works for all of them.

Create Dockerfile in your project root:

# Use the official Node.js 18 image as the base
# alpine is a minimal Linux distribution — smaller image size
FROM node:18-alpine

# Set the working directory inside the container
# All subsequent commands run from this directory
WORKDIR /app

# Copy package.json and package-lock.json first
# Docker caches layers — copying package files separately means
# npm install only reruns when dependencies change, not on every code change
COPY package*.json ./

# Install dependencies
# --production skips devDependencies
RUN npm install --production

# Copy the rest of your application code
COPY . .

# Expose the port your API Gateway listens on
# This is documentation — it does not actually publish the port
EXPOSE 3000

# Default command to run when container starts
# Can be overridden in docker-compose.yml per service
CMD ["node", "-r", "dotenv/config", "node_modules/.bin/moleculer-runner", "services"]

Create a .dockerignore file to prevent unnecessary files from being copied into the image:

node_modules
.env
logs
*.log
.git
.gitignore
README.md

This is like .gitignore but for Docker. It makes builds faster and images smaller.


Understanding the Application Architecture with Docker

Before writing docker-compose.yml, understand what containers you need:

Containers:
  api          — API Gateway service (port 3000 exposed to outside)
  user         — User service (internal only)
  order        — Order service (internal only)
  product      — Product service (internal only)
  email        — Email service (internal only)
  notification — Notification service (internal only)
  mongo        — MongoDB database
  redis        — Redis for caching and transporter
  jaeger       — Jaeger for distributed tracing (optional)

Network:
  All containers on the same Docker network
  They communicate using container names as hostnames

Volumes:
  mongo-data   — MongoDB data persists even when container restarts
  redis-data   — Redis data persists

In this setup, each Moleculer service runs in its own container. They all connect through Redis as the transporter. This is the correct production microservices architecture.


Writing docker-compose.yml

Create docker-compose.yml in your project root:

version: "3.8"

# Named volumes — data persists between container restarts
volumes:
  mongo-data:
  redis-data:

# All containers on this network can reach each other by service name
networks:
  moleculer-net:
    driver: bridge

services:

  # MongoDB database
  mongo:
    image: mongo:6
    container_name: mongo
    restart: unless-stopped
    environment:
      MONGO_INITDB_DATABASE: moleculer-course
    volumes:
      # Persist MongoDB data on your machine
      - mongo-data:/data/db
    networks:
      - moleculer-net
    # Not exposed to outside — only accessible within Docker network
    ports:
      - "27017:27017"  # Expose for local MongoDB Compass access

  # Redis — used as both cacher and transporter
  redis:
    image: redis:7-alpine
    container_name: redis
    restart: unless-stopped
    volumes:
      - redis-data:/data
    networks:
      - moleculer-net
    ports:
      - "6379:6379"  # Expose for local Redis inspection

  # Jaeger — distributed tracing UI
  jaeger:
    image: jaegertracing/all-in-one:latest
    container_name: jaeger
    restart: unless-stopped
    networks:
      - moleculer-net
    ports:
      - "6831:6831/udp"   # Jaeger agent port for traces
      - "16686:16686"     # Jaeger UI — open in browser

  # API Gateway — the only service exposed to the outside world
  api:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: api
    restart: unless-stopped
    environment:
      # Service-specific environment variables
      SERVICES: api           # Only load api.service.js
      PORT: 3000
      NODE_ENV: production
      MONGO_URI: mongodb://mongo:27017/moleculer-course
      REDIS_URI: redis://redis:6379
      JWT_SECRET: your-secret-change-this-in-production
      JAEGER_HOST: jaeger
    depends_on:
      - mongo
      - redis
      - jaeger
    networks:
      - moleculer-net
    ports:
      - "3000:3000"   # Only this service is exposed to the internet
    labels:
      - "service=api-gateway"

  # User service
  user:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: user
    restart: unless-stopped
    environment:
      SERVICES: user          # Only load user.service.js
      NODE_ENV: production
      MONGO_URI: mongodb://mongo:27017/moleculer-course
      REDIS_URI: redis://redis:6379
      JWT_SECRET: your-secret-change-this-in-production
      JAEGER_HOST: jaeger
    depends_on:
      - mongo
      - redis
    networks:
      - moleculer-net

  # Product service
  product:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: product
    restart: unless-stopped
    environment:
      SERVICES: product
      NODE_ENV: production
      MONGO_URI: mongodb://mongo:27017/moleculer-course
      REDIS_URI: redis://redis:6379
      JAEGER_HOST: jaeger
    depends_on:
      - mongo
      - redis
    networks:
      - moleculer-net

  # Order service
  order:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: order
    restart: unless-stopped
    environment:
      SERVICES: order
      NODE_ENV: production
      MONGO_URI: mongodb://mongo:27017/moleculer-course
      REDIS_URI: redis://redis:6379
      JAEGER_HOST: jaeger
    depends_on:
      - mongo
      - redis
    networks:
      - moleculer-net

  # Email service
  email:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: email
    restart: unless-stopped
    environment:
      SERVICES: email
      NODE_ENV: production
      REDIS_URI: redis://redis:6379
    depends_on:
      - redis
    networks:
      - moleculer-net

  # Notification service
  notification:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: notification
    restart: unless-stopped
    environment:
      SERVICES: notification
      NODE_ENV: production
      REDIS_URI: redis://redis:6379
    depends_on:
      - redis
    networks:
      - moleculer-net

The SERVICES Environment Variable — How One Dockerfile Runs Different Services

Notice that every service container uses the same Dockerfile but a different SERVICES environment variable. This is the key pattern.

The moleculer-runner command reads the SERVICES environment variable to decide which service files to load. So:

SERVICES: api     → loads only services/api.service.js
SERVICES: user    → loads only services/user.service.js
SERVICES: order   → loads only services/order.service.js

One Docker image, many containers, each running a different service. This is clean and efficient.


Updating moleculer.config.js for Docker

Your config needs to read from environment variables so it works both locally and in Docker:

"use strict";

require("dotenv").config();

module.exports = {
    namespace: "ecommerce",
    nodeID: null,

    logger: {
        type: "Console",
        options: {
            formatter: process.env.NODE_ENV === "production" ? "json" : "full",
            colors: process.env.NODE_ENV !== "production",
            autoPadding: true
        }
    },

    logLevel: process.env.LOG_LEVEL || "info",

    // Redis as transporter — connects all service containers
    // When REDIS_URI is not set, fall back to null (single process mode)
    transporter: process.env.REDIS_URI
        ? `redis://${process.env.REDIS_URI.replace("redis://", "")}`
        : null,

    // Redis as cacher
    cacher: process.env.REDIS_URI
        ? {
            type: "Redis",
            options: {
                redis: process.env.REDIS_URI,
                ttl: 30
            }
        }
        : {
            type: "Memory",
            options: { ttl: 30 }
        },

    // Metrics
    metrics: {
        enabled: process.env.NODE_ENV === "production",
        reporter: [
            {
                type: "Prometheus",
                options: {
                    port: 3030,
                    path: "/metrics"
                }
            }
        ]
    },

    // Tracing
    tracing: {
        enabled: true,
        exporter: process.env.JAEGER_HOST
            ? [
                {
                    type: "Jaeger",
                    options: {
                        host: process.env.JAEGER_HOST,
                        port: 6832,
                        sampler: {
                            type: "Const",
                            options: { decision: 1 }
                        }
                    }
                }
            ]
            : [
                {
                    type: "Console",
                    options: { colors: true, width: 100, gaugeWidth: 40 }
                }
            ]
    },

    // Fault tolerance
    requestTimeout: 10 * 1000,

    retryPolicy: {
        enabled: true,
        retries: 3,
        delay: 100,
        maxDelay: 2000,
        factor: 2,
        check: err => err && !!err.retryable
    },

    circuitBreaker: {
        enabled: true,
        threshold: 0.5,
        minRequestCount: 20,
        windowTime: 60,
        halfOpenTime: 10 * 1000,
        check: err => err && err.code >= 500
    },

    bulkhead: {
        enabled: true,
        concurrency: 10,
        maxQueueSize: 100
    }
};

Also update your service files to read MONGO_URI from environment:

// In every service that uses MongoDB
adapter: new MongooseAdapter(
    process.env.MONGO_URI || "mongodb://localhost:27017/moleculer-course"
),

Running the Application

Build all images and start all containers:

docker-compose up --build

The first run takes a few minutes as Docker downloads base images and builds your application images. Subsequent runs are much faster because Docker caches layers.

You will see logs from all containers interleaved in the terminal. Each line is prefixed with the container name so you can tell which service produced it.

To run in the background:

docker-compose up --build -d

Check all containers are running:

docker-compose ps

You should see all containers with status Up.


Useful Docker Compose Commands

docker-compose up --build       Start everything and rebuild images
docker-compose up -d            Start in background (detached mode)
docker-compose down             Stop and remove all containers
docker-compose down -v          Stop containers and delete volumes (wipes data)
docker-compose ps               List running containers and their status
docker-compose logs             Show logs from all containers
docker-compose logs user        Show logs from only the user container
docker-compose logs -f user     Follow logs in real time from user container
docker-compose restart user     Restart only the user container
docker-compose exec user sh     Open a shell inside the user container
docker-compose build            Rebuild images without starting
docker-compose stop             Stop containers without removing them
docker-compose start            Start previously stopped containers

Scaling a Service

One of the best features of this architecture is scaling. If your product service is getting too many requests, run three instances with one command:

docker-compose up --scale product=3 -d

Now three product containers are running. They all connect to Redis as the transporter. Moleculer automatically distributes requests across all three using RoundRobin load balancing.

No code changes. No configuration changes. Just one command.


Checking That Everything Works

After docker-compose up, test your endpoints:

Register a user:

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

Login:

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

Create a product:

POST http://localhost:3000/api/product
Body: {
    "name": "Mechanical Keyboard",
    "price": 2999,
    "stock": 50
}

Place an order (with JWT token in Authorization header):

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

Open Jaeger at http://localhost:16686 and search for traces. Select the ecommerce service from the dropdown and click Find Traces. You will see the full request chain visualized as a timeline.


Complete Final Folder Structure

Your project at the end of this post:

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
  logs/
    (generated at runtime)
  Dockerfile
  docker-compose.yml
  .dockerignore
  moleculer.config.js
  package.json
  .env
  .gitignore

Common Issues and Fixes

Issue: Container exits immediately

Check logs:

docker-compose logs <service-name>

Usually a missing environment variable or connection refused to MongoDB or Redis.

Issue: Services cannot connect to MongoDB

Make sure MONGO_URI uses the container name as hostname not localhost:

# Wrong inside Docker
MONGO_URI: mongodb://localhost:27017/moleculer-course

# Correct inside Docker — use container name
MONGO_URI: mongodb://mongo:27017/moleculer-course

Issue: npm install fails during build

Make sure .dockerignore excludes node_modules so Docker does a clean install inside the container.

Issue: Changes to code not reflected

Rebuild the image:

docker-compose up --build

Summary

  • Docker packages your application into portable containers that run the same everywhere.
  • Dockerfile defines how to build your application image. One Dockerfile works for all services.
  • The SERVICES environment variable tells moleculer-runner which service to load in each container.
  • docker-compose.yml defines all containers, their environment variables, networks, and volumes.
  • All containers on the same Docker network communicate using container names as hostnames.
  • Use mongo and redis as hostnames inside Docker, not localhost.
  • Redis serves as both the transporter and cacher in the containerized setup.
  • Volumes persist data between container restarts. Without them data is lost.
  • docker-compose up --build starts everything. docker-compose down stops everything.
  • Scaling a service is one command: docker-compose up --scale product=3.
  • Jaeger runs as a container and is accessible at localhost:16686.

Up Next

Post 14 is the final post — the complete e-commerce project review, best practices, what to learn next, and how to take your Moleculer knowledge to production. We tie together everything from all 13 previous posts into a complete picture.


Course Progress: 13 of 15 posts complete.

Phase 8 - Observability | Post 12 | Logging, Metrics, and Tracing — Understanding What Is Happening Inside Your App

Post 12 of 15 | Phase 8: Observability


Logging, Metrics, and Tracing — Understanding What Is Happening Inside Your App

You have built a working microservices backend. Services are communicating, data is persisting to MongoDB, authentication is working. Now comes the question that every developer faces in production: when something goes wrong, how do you find out what happened and where?

In a monolith you add a console.log and check one log file. In microservices you have ten services running, each producing its own logs, handling hundreds of requests per second. You need proper tools to make sense of it all.

Observability has three pillars:

  • Logging — what happened and when
  • Metrics — how is the system performing right now
  • Tracing — how did a specific request travel through the system

Moleculer has built-in support for all three.


Pillar 1: Logging

You have been using this.logger throughout the course. Let us now understand logging properly and set it up for real use.

Log Levels

Moleculer has six log levels in order from most to least verbose:

trace   — extremely detailed, every internal step
debug   — debugging information, variable values
info    — normal operational messages, service started, action called
warn    — something unexpected happened but the app is still running
error   — something failed, needs attention
fatal   — critical failure, app may not recover

In development use info or debug. In production use warn or error to reduce noise and storage cost.

Configuring the Logger in moleculer.config.js

module.exports = {
    logger: {
        type: "Console",
        options: {
            // Show colors in terminal output
            colors: true,

            // Different colors for different services
            moduleColors: true,

            // full shows timestamp, level, nodeID, service name, message
            // short shows less information
            // simple shows just level and message
            // json outputs machine-readable JSON — use in production
            formatter: "full",

            // Automatically pad service names so logs align in columns
            autoPadding: true
        }
    },

    logLevel: "info"
};

Using the Logger in Services

module.exports = {
    name: "order",

    actions: {
        async create(ctx) {
            // Different log levels for different situations
            this.logger.debug("create action called", ctx.params);
            this.logger.info(`Creating order for user ${ctx.meta.user.id}`);

            try {
                const order = await this.adapter.insert(ctx.params);
                this.logger.info(`Order created successfully: ${order._id}`);
                return order;
            } catch (err) {
                this.logger.error("Failed to create order", err);
                throw err;
            }
        }
    }
};

Multiple Loggers at Once

In production you often want logs to go to both the console and a file or external service. Moleculer supports multiple loggers simultaneously:

module.exports = {
    logger: [
        // Console logger for development visibility
        {
            type: "Console",
            options: {
                formatter: "short",
                colors: true
            }
        },

        // File logger — writes to a file
        {
            type: "File",
            options: {
                // Log file location
                filename: "logs/moleculer-{date}.log",

                // New file every day
                eol: "\n",

                // Format as JSON for log parsing tools
                formatter: "json"
            }
        }
    ],

    logLevel: {
        // Different levels for different services
        // "*" is the default for all services
        "*": "warn",

        // Show more detail for the order service
        "ORDER": "debug",

        // Show all logs from the broker itself
        "BROKER": "info"
    }
};

Structured Logging — Logging Objects Not Strings

In production, logs are parsed by tools like Datadog, Elasticsearch, or CloudWatch. These tools work best with structured JSON logs. Always log objects alongside messages instead of concatenating strings:

// Bad — hard to parse programmatically
this.logger.info("Order " + orderId + " created for user " + userId + " with total " + total);

// Good — structured, easy to parse and filter
this.logger.info("Order created", {
    orderId,
    userId,
    total,
    productId,
    timestamp: new Date().toISOString()
});

Pillar 2: Metrics

Metrics tell you how your system is performing right now. Things like:

  • How many requests per second is each service handling
  • What is the average response time of each action
  • How many errors are occurring
  • How much memory is being used
  • How many active connections exist

Moleculer has a built-in metrics system that can expose these numbers to external monitoring tools like Prometheus and Grafana.

Enabling Metrics in moleculer.config.js

module.exports = {
    metrics: {
        enabled: true,

        // Where to send metrics
        reporter: [
            // Console reporter — prints metrics to terminal periodically
            {
                type: "Console",
                options: {
                    // Print metrics every 10 seconds
                    interval: 10
                }
            },

            // Prometheus reporter — exposes metrics at an HTTP endpoint
            // Grafana reads from Prometheus to create dashboards
            {
                type: "Prometheus",
                options: {
                    // Metrics available at http://localhost:3030/metrics
                    port: 3030,
                    path: "/metrics",

                    // Default labels added to every metric
                    defaultLabels: (registry) => ({
                        namespace: registry.broker.namespace,
                        nodeID: registry.broker.nodeID
                    })
                }
            }
        ]
    }
};

Install the Prometheus reporter:

npm install @moleculer/lab

Actually for Prometheus you need:

npm install moleculer-prometheus

When Prometheus metrics are enabled, visit http://localhost:3030/metrics and you will see output like this:

# HELP moleculer_request_total Total count of requests
# TYPE moleculer_request_total counter
moleculer_request_total{action="user.login",nodeID="node-1"} 42

# HELP moleculer_request_time_milliseconds Request time in milliseconds
# TYPE moleculer_request_time_milliseconds histogram
moleculer_request_time_milliseconds_bucket{action="user.login",le="10"} 38
moleculer_request_time_milliseconds_bucket{action="user.login",le="100"} 42

# HELP moleculer_request_error_total Total count of request errors
# TYPE moleculer_request_error_total counter
moleculer_request_error_total{action="user.login",nodeID="node-1"} 2

Prometheus scrapes this endpoint periodically and stores the data. Grafana connects to Prometheus and displays it as beautiful dashboards.

Built-in Metrics Moleculer Tracks Automatically

When metrics are enabled, Moleculer automatically tracks these without any code from you:

moleculer.request.total           — total requests per action
moleculer.request.active          — currently processing requests
moleculer.request.error.total     — total errors per action
moleculer.request.time            — response time histogram per action
moleculer.event.total             — total events emitted
moleculer.transporter.sent.total  — messages sent via transporter
moleculer.process.memory.heap     — Node.js heap memory usage
moleculer.process.cpu             — CPU usage percentage
moleculer.os.memory.free          — free system memory

All of this is tracked automatically. Zero code needed.

Custom Metrics

You can also track your own business metrics:

module.exports = {
    name: "order",

    async started() {
        // Register a custom counter metric
        this.broker.metrics.register({
            type: "counter",
            name: "order.created.total",
            labelNames: ["status"],
            description: "Total orders created"
        });

        // Register a custom gauge metric
        this.broker.metrics.register({
            type: "gauge",
            name: "order.revenue.total",
            description: "Total revenue from orders"
        });
    },

    actions: {
        create: {
            async handler(ctx) {
                const order = await this.adapter.insert(ctx.params);

                // Increment your custom counter
                this.broker.metrics.increment("order.created.total", {
                    status: "confirmed"
                });

                // Update your custom gauge
                this.broker.metrics.increment(
                    "order.revenue.total",
                    null,
                    order.totalPrice
                );

                return order;
            }
        }
    }
};

Now your Prometheus dashboard shows business metrics alongside system metrics. You can see total orders created and total revenue in real time.


Pillar 3: Tracing

This is the most powerful observability tool for microservices. Tracing shows you the complete journey of a single request as it travels through multiple services.

Without tracing, when a request is slow or fails, you know something went wrong but not where. With tracing you can see:

  • request started at API Gateway at 10:00:00.000
  • API Gateway called order.create at 10:00:00.005 — took 250ms
  • order.create called user.get at 10:00:00.010 — took 50ms
  • order.create called product.get at 10:00:00.060 — took 30ms
  • order.create called product.reduceStock at 10:00:00.090 — took 80ms
  • order.create completed at 10:00:00.255
  • API Gateway responded at 10:00:00.260

You can see exactly where time was spent. If product.get took 5 seconds suddenly, you know immediately where to look.

Enabling Tracing in moleculer.config.js

module.exports = {
    tracing: {
        enabled: true,

        exporter: [
            // Console exporter — prints trace spans to terminal
            // Good for development
            {
                type: "Console",
                options: {
                    // Only log spans that take more than 5ms
                    // Reduces noise from fast operations
                    logger: null,
                    colors: true,
                    width: 100,
                    gaugeWidth: 40
                }
            },

            // Jaeger exporter — sends traces to Jaeger UI
            // Best tool for visualizing traces in development
            {
                type: "Jaeger",
                options: {
                    // Jaeger collector endpoint
                    endpoint: null,
                    host: "localhost",
                    port: 6832,

                    // Send all spans (1.0 = 100%, 0.1 = 10%)
                    sampler: {
                        type: "Const",
                        options: { decision: 1 }
                    }
                }
            }
        ]
    }
};

Setting Up Jaeger Locally

Jaeger is the most popular distributed tracing tool. Run it with Docker (we cover Docker fully in Post 14, but use this command now if you have Docker):

docker run -d --name jaeger \
  -p 6831:6831/udp \
  -p 16686:16686 \
  jaegertracing/all-in-one:latest

Then open http://localhost:16686 in your browser. This is the Jaeger UI where you can visualize traces.

If you do not have Docker yet, use the Console exporter for now and come back to Jaeger in Post 14.

Console Tracing Output

With the Console exporter enabled, when you make a request you will see output like this in your terminal:

=== REQUEST TRACE ===
api.rest                          ████████████████████████ 255ms
  order.create                      ██████████████████ 245ms
    user.get                          ████ 50ms
    product.get                       ███ 30ms
    product.reduceStock               ██████ 80ms

This visual representation shows you the full call chain and how long each step took.

Adding Custom Trace Spans

When you have a complex operation inside a single action, you can break it into named spans to see exactly where time is spent:

module.exports = {
    name: "order",

    actions: {
        create: {
            async handler(ctx) {
                // Start a custom span for the validation step
                const validationSpan = this.broker.tracer.startSpan(
                    "Validate order data",
                    { parentSpan: ctx.span }
                );

                // Do validation work
                await this.validateOrderData(ctx.params);

                // End the validation span
                validationSpan.finish();

                // Start a span for the database insert
                const dbSpan = this.broker.tracer.startSpan(
                    "Save order to database",
                    { parentSpan: ctx.span }
                );

                const order = await this.adapter.insert(ctx.params);

                dbSpan.finish();

                return order;
            }
        }
    }
};

Now in Jaeger you will see the order.create span broken into sub-spans showing exactly how long validation took versus database insertion.

Adding Tags to Spans

Tags let you attach metadata to a span so you can filter and search traces:

actions: {
    create: {
        async handler(ctx) {
            // Add tags to the current span
            ctx.span.setTag("userId", ctx.meta.user.id);
            ctx.span.setTag("productId", ctx.params.productId);
            ctx.span.setTag("quantity", ctx.params.quantity);

            const order = await this.adapter.insert(ctx.params);

            ctx.span.setTag("orderId", order._id.toString());

            return order;
        }
    }
}

In Jaeger you can search for traces by tag. Find all orders for a specific user, or all orders for a specific product, by searching the tag values.


Complete Observability Configuration

Here is a complete moleculer.config.js with all three pillars enabled for production:

"use strict";

require("dotenv").config();

module.exports = {
    namespace: "ecommerce",
    nodeID: null,

    // Logging
    logger: [
        {
            type: "Console",
            options: {
                formatter: "full",
                colors: true,
                moduleColors: true,
                autoPadding: true
            }
        },
        {
            type: "File",
            options: {
                filename: "logs/moleculer-{date}.log",
                formatter: "json"
            }
        }
    ],

    logLevel: {
        "*": "warn",
        "BROKER": "info",
        "REGISTRY": "info"
    },

    // Metrics
    metrics: {
        enabled: true,
        reporter: [
            {
                type: "Prometheus",
                options: {
                    port: 3030,
                    path: "/metrics",
                    defaultLabels: (registry) => ({
                        namespace: registry.broker.namespace,
                        nodeID: registry.broker.nodeID
                    })
                }
            }
        ]
    },

    // Tracing
    tracing: {
        enabled: true,
        exporter: [
            {
                type: "Console",
                options: {
                    colors: true,
                    width: 100,
                    gaugeWidth: 40
                }
            },
            {
                type: "Jaeger",
                options: {
                    host: process.env.JAEGER_HOST || "localhost",
                    port: 6832,
                    sampler: {
                        type: "Const",
                        options: { decision: 1 }
                    }
                }
            }
        ]
    },

    // Fault tolerance
    requestTimeout: 10 * 1000,

    retryPolicy: {
        enabled: true,
        retries: 3,
        delay: 100,
        maxDelay: 2000,
        factor: 2,
        check: err => err && !!err.retryable
    },

    circuitBreaker: {
        enabled: true,
        threshold: 0.5,
        minRequestCount: 20,
        windowTime: 60,
        halfOpenTime: 10 * 1000,
        check: err => err && err.code >= 500
    },

    bulkhead: {
        enabled: true,
        concurrency: 10,
        maxQueueSize: 100
    },

    // Caching
    cacher: {
        type: "Memory",
        options: {
            ttl: 30
        }
    },

    transporter: null
};

What to Do When Something Goes Wrong in Production

Here is a practical debugging workflow using all three pillars together:

Step 1: Check metrics first. Look at your Grafana dashboard. Which action has a spike in error rate or response time? Suppose you see order.create response time jumped from 50ms to 5000ms.

Step 2: Open Jaeger and filter traces for order.create in the last 30 minutes. Sort by duration. Open the slowest trace.

Step 3: The trace shows order.create called product.get and that took 4900ms. Everything else was normal.

Step 4: Check your logs filtered by the requestID from the slow trace. You see a MongoDB query timeout in the product service.

Step 5: Fix the MongoDB index on the product collection. Response time drops back to normal.

In five steps you went from knowing something is wrong to knowing exactly what and why. Without observability this could take hours.


Summary

  • Observability has three pillars: Logging, Metrics, Tracing.
  • Logging records what happened. Use structured JSON logging in production. Use appropriate log levels.
  • Multiple loggers can run simultaneously — console for development, file for production.
  • logLevel can be set per service for fine-grained control.
  • Metrics track system performance numbers. Moleculer tracks request counts, response times, errors, and memory automatically.
  • Prometheus reporter exposes metrics at an HTTP endpoint. Grafana reads Prometheus to create dashboards.
  • Custom metrics let you track business numbers like total orders and total revenue.
  • Tracing records the complete journey of each request through all services.
  • Each hop in the call chain creates a span. Spans are organized into a trace tree.
  • Jaeger is the best tool for visualizing traces locally.
  • Use ctx.span.setTag() to attach searchable metadata to traces.
  • The debugging workflow: metrics to find the problem, tracing to locate it, logging to understand it.

Up Next

Post 13 covers Docker and Docker Compose — packaging each service into a container and running the entire application with one command. This is the final step before production deployment. We will containerize all our services, add MongoDB and Redis containers, and wire everything together with a transporter.


Course Progress: 12 of 15 posts complete.

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.

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...