Centralizing Logging and Auth in Moleculer with Action Middleware
Stop copying auth and logging boilerplate into every Moleculer action. This guide builds composable action middleware, wires it into a service, and verifies it with real broker calls.
13 Aug 2025, 23:38 UTC

You have a dozen services in a Moleculer system, and every action that needs authentication or request logging carries the same copied-and-pasted preamble. The useful takeaway: Moleculer lets you wrap actions with middleware so cross-cutting concerns live in one place, and each action handler stays focused on its actual business logic. This guide shows how to build a logging and an authorization middleware, attach them to actions, and verify they actually run.
What action middleware does in Moleculer
Moleculer is a Node.js microservices framework where services expose actions — named, callable functions addressed over a transporter or locally. Every action call produces a Context object (ctx) carrying the params, metadata, the caller's identity fields, and helpers like ctx.call for invoking other actions.
Middleware wraps the action handler. It runs before the handler (where you can reject the call, log the request, or enrich the context) and after it (where you can inspect or transform ctx.result). Because the wrapper sees the same ctx as the handler, it can make decisions based on params, meta, or the calling service, then either pass control along or short-circuit with an error.
Version note: the middleware APIs described here assume Moleculer v0.14, the long-standing stable line. If you are on a newer major version, check the release notes for your version — the middleware registration API has changed across majors.
Prerequisites
- Node.js 14 or newer and a project with
moleculerinstalled (npm install moleculer). - A working broker you can start locally; no transporter is needed for this guide since everything runs in one process.
- Basic familiarity with defining a service and calling an action with
broker.call.
Step 1: Write a logging middleware
In Moleculer v0.14, local action middleware is a function that receives the handler and returns a new handler. The wrapper gets ctx, does its "before" work, awaits the real handler, then does its "after" work:
// middlewares.js
function actionLogger(logger) {
return function (handler) {
return async function (ctx) {
const start = Date.now();
logger.info(`--> ${ctx.action.name} params=%j`, ctx.params);
try {
const result = await handler(ctx);
logger.info(`<-- ${ctx.action.name} ok in ${Date.now() - start}ms`);
return result;
} catch (err) {
logger.warn(`<-- ${ctx.action.name} failed: ${err.message}`);
throw err; // re-throw so callers still see the real error
}
};
};
}
module.exports = { actionLogger };
Two details matter. First, always re-throw errors after logging — swallowing them turns failures into silent undefined results. Second, be careful logging ctx.params in production; redact fields like passwords or tokens before writing them to logs.
Step 2: Write an authorization middleware
A common pattern is passing the authenticated user's role in ctx.meta (set by your API gateway service) and checking it before the handler runs. Moleculer ships a MoleculerClientError / MoleculerError family; throwing one short-circuits the action and propagates a typed error to the caller:
const { Errors } = require("moleculer");
function requireRole(role) {
return function (handler) {
return async function (ctx) {
if (ctx.meta.userRole !== role) {
throw new Errors.MoleculerClientError(
"Forbidden: requires role " + role, 403, "FORBIDDEN");
}
return handler(ctx);
};
};
}
Because the middleware is a factory (requireRole("admin")), you can reuse it with different roles across actions instead of writing one middleware per role.
Step 3: Attach middleware to a service
Moleculer v0.14 supports middleware declared at the service level via the middlewares hook or applied per action by wrapping the handler. The most explicit, version-tolerant approach is wrapping handlers directly when defining the service:
// orders.service.js
const { actionLogger } = require("./middlewares");
const { requireRole } = require("./middlewares");
const wrap = (...mws) => (handler) =>
mws.reduceRight((h, mw) => mw(h), handler);
module.exports = {
name: "orders",
actions: {
async create(ctx) {
// real business logic only — no auth or logging boilerplate
return { id: 1, item: ctx.params.item };
}
},
created() {
const withCrossCutting = wrap(
actionLogger(this.logger),
requireRole("admin")
);
this.actions.create.handler = withCrossCutting(this.actions.create.handler);
}
};
The reduceRight composition means the logger runs outermost: it sees the full call including auth rejections, which is usually what you want for audit trails. Swap the order if you only want to log calls that pass authorization.
Step 4: Verify the middleware actually runs
Start a broker, load the service, and call the action twice — once without a role and once with the right one. Run this in a Node script or REPL in your project directory; no special permissions are needed:
const { ServiceBroker } = require("moleculer");
const broker = new ServiceBroker({ logLevel: "info" });
broker.createService(require("./orders.service"));
(async () => {
await broker.start();
try {
await broker.call("orders.create", { item: "book" }); // no meta
} catch (err) {
console.log("rejected as expected:", err.code, err.message);
}
const res = await broker.call(
"orders.create", { item: "book" }, { meta: { userRole: "admin" } });
console.log("accepted:", res);
await broker.stop();
})();
Expected checks: the first call is rejected with a 403 client error and the handler never executes; the second returns { id: 1, item: "book" }; and your broker log shows the -->/<-- lines around both attempts. To confirm the observed behavior comes from the middleware and not something else, temporarily remove the wrapper and re-run — both calls should then succeed with no log lines.
Limitations and trade-offs
- Middleware adds latency to every wrapped call. Keep the "before" phase cheap; anything involving network I/O (like a remote token introspection) should be cached or done once at the gateway.
- Per-action wrapping in
created()is explicit but easy to forget for new actions. For broker-wide coverage, look at Moleculer's broker-levelmiddlewaresoption, which wraps all local actions — but verify its exact shape against your installed version. - Middleware ordering is significant and not validated by the framework. Document the intended order where you compose them.
- Errors thrown in middleware skip the handler entirely, which is the point for auth — but a bug in a logging middleware can take down otherwise healthy actions, so keep middleware code minimal and unit-test it in isolation.
When this pays off
The decision to centralize cross-cutting concerns in middleware pays off once more than two or three actions share the same concern. Below that, inline checks are simpler to read. Above it, middleware gives you one audited place for auth policy and log format, and action handlers that read as pure business logic — which is the real maintainability win in a growing Moleculer system.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.