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.

No comments:

Post a Comment

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

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