Leader election module for NestJS using the Raft algorithm.
This package does not bundle Nest packages. Align versions with Nest 11.x (^11.0.0; minimum satisfies 11.0.0). Your app must provide a single resolved copy of @nestjs/common / @nestjs/core (same major as peers). Installing them twice—for example nested under node_modules/nestjs-raft-leader-election and at the app root—breaks the global DI container (e.g. Reflector missing for @nestjs/schedule / SchedulerMetadataAccessor).
RaftModule calls ScheduleModule.forRoot() and HeartbeatService uses @Interval, so @nestjs/schedule is required (install it in the app alongside Nest core).
Express-based apps normally already have @nestjs/platform-express; pin it so it matches @nestjs/common / @nestjs/core.
pnpm add nestjs-raft-leader-election @nestjs/common @nestjs/core @nestjs/platform-express @nestjs/schedule
# npm install nestjs-raft-leader-election @nestjs/common @nestjs/core @nestjs/platform-express @nestjs/scheduleAfter install, pnpm why @nestjs/core (or npm equivalent) should show one physical @nestjs/core for the workspace, not a second tree only inside this library.
Redis must be reachable from every replica that participates in election.
Import RaftModule and call forRoot with Redis connection options:
import { Module } from "@nestjs/common";
import { RaftModule } from "nestjs-raft-leader-election";
@Module({
imports: [
RaftModule.forRoot({
redis: {
url: process.env.REDIS_URL ?? "redis://localhost:6379",
password: process.env.REDIS_PASSWORD, // optional
},
// Required: isolate pub/sub per logical app (`my-service:heartbeat`, …).
namespace: process.env.RAFT_REDIS_NAMESPACE ?? "my-service",
}),
],
})
export class AppModule {}Use forRootAsync when Redis URL (or password) comes from @nestjs/config instead of literals. Install config package alongside this library:
pnpm add @nestjs/configLoad ConfigModule first so ConfigService is available to the raft factory (imports + inject pattern below matches Nest configurable modules):
import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { RaftModule } from "nestjs-raft-leader-election";
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
RaftModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
redis: {
url: config.getOrThrow<string>("REDIS_URL"),
password: config.get<string>("REDIS_PASSWORD"),
},
namespace: config.getOrThrow<string>("RAFT_REDIS_NAMESPACE"),
}),
}),
],
})
export class AppModule {}If RaftModule.forRootAsync is the first consumer of ConfigService, you can omit imports: [ConfigModule] when configuration is globally registered (isGlobal: true).
RaftModule is registered as a global module, so after a single forRoot / forRootAsync import (for example on AppModule), HeartbeatService is injectable in any module without importing RaftModule again.
Inject HeartbeatService where you need to run code only on the elected leader (cron, background jobs, single-writer paths).
import { Injectable } from "@nestjs/common";
import { HeartbeatService } from "nestjs-raft-leader-election";
@Injectable()
export class JobsService {
constructor(private readonly heartbeat: HeartbeatService) {}
async runIfLeader(): Promise<void> {
if (!this.heartbeat.isLeader()) {
return;
}
// leader-only work
}
}HeartbeatService starts Redis subscriptions on application bootstrap and publishes heartbeats on a fixed interval; isLeader() compares this instance’s node id with the current leader announced over Redis.
Operational note: All replicas that participate in one leader election must use the same Redis and the same namespace. Channels are ${namespace}:heartbeat, ${namespace}:election, ${namespace}:vote, and ${namespace}:leader. namespace must be non-empty (whitespace trimmed); use distinct values when several apps share one Redis server.
$ pnpm install$ pnpm run build# development
$ pnpm run start
# watch mode
$ pnpm run start:dev
# production mode
$ pnpm run start:prod# unit tests
$ pnpm run test
# e2e tests
$ docker run -p 6379:6379 redis
$ pnpm run test:e2e
# test coverage
$ pnpm run test:covThis project is MIT licensed.