How to define a Moleculer service action with parameter validation and reliable timeout/retry handling
Define a Moleculer service action with parameter validation and reliable timeout/retry handling to avoid ValidationErrors and unsafe retries.
06 Jun 2026, 15:52 UTC

Problem
You need to expose a Microservice action that only processes correctly‑typed parameters and that can survive temporary network glitches without risking duplicate side‑effects. In a typical Moleculer setup the broker validates incoming payloads before the handler runs, but if validation is missing or mis‑configured the action may receive invalid data, cause a ValidationError, or retry a non‑idempotent operation after a timeout.
Prerequisites
- Node.js (v18+) installed and a project initialized with
npm init -y. - Moleculer package (
npm i moleculer) and its broker constructor. - A validator package – FastestValidator (default) or Ajv – listed in
package.json. - Access to edit files in the
services/directory and run the broker with sufficient permissions (e.g., a user that can start the Node process).
Step‑by‑step procedure
- Create a service file (e.g.,
services/UserService.js). The file must export a class that extendsMoleculer.Serviceand define aregistermethod. Insideregisterusethis.helper('validate', ...)or rely on the broker’s built‑in validator. - Define a schema for the `params` object. Moleculer passes the
paramsargument to the handler; the schema is evaluated before the handler runs. A minimal schema for a numericidand a stringemaillooks like this: - Attach the schema to the action by returning it from the
registermethod (or by usingthis.helper('validate', ...)with a custom validator). Example: - Invoke the action with timeout and retry options. The
optsargument ofctx.calllets you settimeout(ms) andretries(number). Example call: - Handle the ValidationError. Because validation runs before the handler, a
ValidationError(MoleculerError, code 400) is thrown immediately. Your catch block should inspect the error message to see which fields are invalid and return a clear response to the caller. - Verify retry behavior. To ensure retries are counted correctly, stop the target service while the call is in flight:
- Run the broker with a minimal service that includes the schema above. Call the action with a missing
idfield. Assert that aValidationErroris thrown before any log entry from the handler appears. - Start the service, then invoke the action with
timeout: 500andretries: 1. Immediately terminate the service. Verify that the broker makes exactly two attempts (initial + one retry) and that the final error is a timeout, not a validation error. - Inspect the broker constructor and
package.jsonto confirm which validator is installed. For Moleculer 3.x the default is FastestValidator; for older versions Ajv may be used. Mismatched versions can cause silent validation failures. - Combine validation with authentication. Validation guarantees data shape but not permission; add role‑based checks inside the handler or via middleware.
- Idempotency. Retries are unsafe for actions that modify state (e.g., creating a charge). If the operation is not idempotent, either make it idempotent or set
retries: 0and handle transient failures upstream. - Timeout tuning. The timeout is best‑effort; under heavy load the event loop may fire early. Use observed p99 latency from monitoring to set a realistic timeout rather than a fixed small value.
- Validator version drift. Different Moleculer releases may ship different validator defaults. Verify the installed package version matches the broker’s expected behavior.
class UserService extends Moleculer.Service {
constructor(broker) {
super(broker);
this.helper('validate', broker.validator);
}
async getUser(ctx) {
// ctx.params already validated
return ctx.params.id ? await this.findUser(ctx.params.id) : null;
}
async createUser(ctx) {
// The `params` schema is defined below
const user = await this.createInDb(ctx.params);
return user;
}
}
module.exports = UserService;
const userSchema = {
type: 'object',
required: ['id'],
properties: {
id: { type: 'integer', minimum: 1 },
email: { type: 'string', format: 'email' }
}
};
async register(ctx) {
// FastestValidator is the default; you can pass your own validator instance
this.helper('validate', ctx, userSchema);
// now the handler can safely use ctx.params
}
try {
const result = await ctx.call('UserService.createUser', {
id: 42,
email: '[contact removed]'
}, { timeout: 1500, retries: 2 });
console.log('Created:', result);
} catch (err) {
// ValidationError appears as MoleculerError with code 400
if (err instanceof MoleculerError && err.code === 400) {
console.error('Parameter validation failed:', err.message);
} else {
console.error('Unexpected error:', err);
}
}
// In a separate terminal
node services/UserService.js # start the service
// In the calling code (or a test script)
await ctx.call('UserService.createUser', { id: 1 }, { timeout: 1000, retries: 3 });
Stop the service (Ctrl‑C) after the request is sent. The broker should attempt the call up to three times (the value of retries plus the initial attempt) and then reject with a timeout error.
Expected checks and verification
Recovery options and cautions
Concrete supported example
Below is a minimal, fully‑functional service that demonstrates validation, timeout, and retry handling. Save it as services/EchoService.js and run the broker with moleculer run (or node broker.js if you use a custom entry point).
// services/EchoService.js
const Moleculer = require('moleculer');
class EchoService extends Moleculer.Service {
constructor(broker) {
super(broker);
// Use FastestValidator (installed via npm i fastest-validator)
this.helper('validate', broker.validator);
}
async echo(ctx) {
// `params` must be a string
this.helper('validate', ctx, { message: 'string' });
return `You said: ${ctx.params.message}`;
}
}
module.exports = EchoService;
To test retries, run the service, then execute from another process:
moleculer call EchoService.echo 'hello' --timeout 2000 --retries 2
Stop the service while the request is pending. The broker will retry twice before returning a timeout error.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.