Choosing a Service Versioning Strategy in Moleculer: Version Property vs. Naming vs. Middleware
When building microservices with Moleculer, deciding how to handle API evolution matters. This guide compares the built‑in version property, name‑based versioning, and custom middleware approaches, then walks through a concrete example and how to verify it works.
20 Jul 2026, 21:06 UTC

Decision Context
In a microservice ecosystem, APIs change over time. Moleculer offers several ways to expose different API revisions without breaking existing consumers. The core question is: Which versioning strategy best fits our team’s workflow, tooling, and deployment pipeline?
Constraints to consider:
- Team’s coding standards and review process.
- Frequency of API changes.
- Need for backward compatibility.
- Operational overhead (build, deployment, monitoring).
- Client library support (e.g., auto‑generation, SDKs).
Versioning Options
| Option | How It Works | Code Reuse | Caller Burden | Broker Complexity |
|---|---|---|---|---|
Built‑in version property |
Service declares version: '1.0' in its definition. Callers pass { version: '1.0' } in the call options. |
High – same file can expose multiple versions by duplicating actions or using guards. | Medium – callers must specify the version explicitly. | Low – broker has built‑in matcher (enabled by default). |
Name‑based versioning (e.g., users.v1) |
Each API revision is a separate service with a distinct name. | Low – duplicated code across services. | Low – callers use the full service name. | Low – no special broker logic required. |
| Custom metadata + middleware | Add meta: { apiVersion: '1.0' } and route calls through a middleware that rewrites the target service based on the caller’s desired version. |
Medium – can centralise logic but adds indirection. | High – middleware abstracts the version away from callers. | High – custom routing logic and potential performance impact. |
Trade‑Off Analysis
Built‑in version property keeps the service definition clean and leverages Moleculer’s native routing. The only cost is that every call must include the version option, which is trivial for automated clients but can be overlooked in manual tests.
Name‑based versioning is the simplest to understand: each service name maps to a single API shape. However, duplicating the same logic in multiple files increases maintenance overhead and can lead to drift between versions.
Custom middleware offers the most flexibility, allowing you to keep a single service name while internally routing to the correct implementation. The downside is added complexity in the broker and the risk of routing loops or mismatches if the middleware logic is buggy.
Implementation Example – Built‑in Version Property
Below is a minimal setup that demonstrates a single service exposing version 1.0 and a client calling it with the version flag.
// services/users.service.js
module.exports = {
name: "users",
version: "1.0", // // Moleculer version tag
actions: {
get: {
async handler(ctx) {
const user = { id: ctx.params.id, name: "John Doe", version: ctx.service.version };
return user;
}
}
}
};
// broker.js
const { ServiceBroker } = require("moleculer");
const broker = new ServiceBroker({ nodeID: "node-1", transporter: "NATS" });
broker.loadService("./services/users.service.js");
broker.start().then(() => {
console.log("Broker started – ready to accept calls");
});
Client call (e.g., from another service or a Node REPL):
// client.js
const { ServiceBroker } = require("moleculer");
const broker = new ServiceBroker({ nodeID: "client-node", transporter: "NATS" });
broker.start().then(async () => {
const result = await broker.call("users.get", { id: 1 }, { version: "1.0" });
console.log(result); // { id: 1, name: "John Doe", version: "1.0" }
});
Verification Checklist
- Broker registration: After
broker.start(), runbroker.getLocalServices()in a REPL. The output should include an entry withname: "users"andversion: "1.0". - Call with version: Execute
broker.call("users.get", { id: 1 }, { version: "1.0" })and assert that the returned object containsversion: "1.0". - Call without version: Try
broker.call("users.get", { id: 1 }). With the default broker configuration, Moleculer will route to the highest available version. If you want to enforce explicit versioning, setbroker.options.versioner = nullor handle it in your code. - Cluster consistency: In a multi‑node cluster, ensure every node runs the same Moleculer major version (≥0.14) and that the
versioneroption is enabled on all brokers.
Caveats & Best Practices
- **Version matcher** – The built‑in
versioneris enabled by default in Moleculer v0.14+. If you disable it (e.g.,broker.options.versioner = null), theversionoption will be ignored, leading to unpredictable routing. - **Rapid evolution** – When APIs change frequently, maintain backward‑compatible logic or provide a migration path. Otherwise, callers may hit breaking changes unexpectedly.
- **Monitoring** – Expose the service’s version in logs or metrics to aid debugging and traceability.
- **Testing** – Write unit tests that explicitly call actions with different
versionoptions to ensure the correct implementation is used. - **Documentation** – Clearly document the required
versionfield in your service contracts so that external consumers know to include it.
Adopting the built‑in version property strikes a good balance between code reuse and explicitness. It leverages Moleculer’s native routing, keeps your service definitions concise, and provides a clear contract for callers. If your team prefers zero‑configuration for consumers, name‑based versioning is a viable alternative, albeit at the cost of duplicated code. Custom middleware offers the most flexibility but should be reserved for complex scenarios where automatic routing logic is essential.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.