Skip to content

NestJS

since: 1.7

@runwisp/js/nestjs has the API of @nestjs/schedule: the same module, decorators and SchedulerRegistry. RunWisp runs the schedule, and your methods run in the app with your injected services.

It works with NestJS 10, 11 and 12, on Node.js 20.19, 22.12 or later, or on Bun.

Terminal window
npm install @runwisp/js

Import RunWispModule once, in the root module:

src/app.module.ts
import { Module } from "@nestjs/common";
import { RunWispModule } from "@runwisp/js/nestjs";
import { DatabaseModule } from "./database.module.js";
import { TasksService } from "./tasks.service.js";
@Module({
imports: [DatabaseModule, RunWispModule.forRoot()],
providers: [TasksService],
})
export class AppModule {}

forRoot() takes the new RunWisp() options. Turn on shutdown hooks in main.ts, so running tasks finish before the app exits:

src/main.ts
const app = await NestFactory.create(AppModule);
app.enableShutdownHooks();

On shutdown, running tasks finish before any onModuleDestroy, beforeApplicationShutdown or onApplicationShutdown hook runs, so a database client closed in one stays open for them. This holds for app.close() too, and whatever the order of imports.

src/tasks.service.ts
import { Injectable, Logger } from "@nestjs/common";
import { Cron, CronExpression, Interval } from "@runwisp/js/nestjs";
import { ReportsService } from "./reports.service.js";
@Injectable()
export class TasksService {
private readonly logger = new Logger(TasksService.name);
constructor(private readonly reports: ReportsService) {}
@Cron(CronExpression.EVERY_DAY_AT_3AM, { name: "nightly-report" })
async sendReport() {
await this.reports.send();
this.logger.log("report sent");
}
@Interval("sync", 30_000)
sync() {}
}

Each method becomes a RunWisp task. Without a name, the task is named ClassName.methodName. What the method logs goes to the run’s log, and a thrown error fails the run.

As with @nestjs/schedule, runs overlap: a run that comes while the last one is still going starts anyway, unless waitForCompletion skips it. Times missed while the app was down are not run late. The tasks get catch_up = 0, and max_concurrent = 1024, or on_overlap = "skip" with waitForCompletion. The method gets the run’s context as its argument, with a signal for stopping.

The app prints the dashboard address on startup.

Replace @nestjs/schedule with @runwisp/js/nestjs in your imports. ScheduleModule is another name for RunWispModule, so the rest of the code stays as it is.

@nestjs/schedule @runwisp/js/nestjs
@Cron(expression, opts) Same. timeZone, waitForCompletion and disabled work. A Date is not supported.
@Interval(ms) Runs every ms milliseconds, in whole seconds: ms is a multiple of 1000.
@Timeout(ms) Runs once when RunWisp starts, with run_on_start.
SchedulerRegistry Jobs are task handles: stop(), start(), getNextRun().
addCronJob, addInterval Not supported. Inject RunWisp and call task().

@Interval throws when the app starts if ms is not a whole number of seconds, since RunWisp would run it at a different interval. @Timeout ignores the delay. @Cron accepts utcOffset, initialDelay, unrefTimeout and threshold so your code compiles, ignores them, and logs a warning once for each. Use timeZone instead of utcOffset.

@nestjs/schedule runs every job in every copy of the app. RunWisp runs each run once per data directory, however many copies run. See Several copies of the app.

The module exports the RunWisp instance. Inject it for tasks with the full set of task options:

import { Injectable, OnModuleInit } from "@nestjs/common";
import { RunWisp } from "@runwisp/js/nestjs";
@Injectable()
export class BackupService implements OnModuleInit {
constructor(private readonly runwisp: RunWisp) {}
onModuleInit() {
this.runwisp.task("backup", { cron: "0 4 * * *", retry_attempts: 3 }, () => this.backup());
}
}

Set RUNWISP_JS=off in your test environment, so tests that boot the app don’t start RunWisp. See Unit tests.

Vitest, the default from Nest 12, loads @runwisp/js as is. Jest needs version 30.4 or later, Node.js 24.9 or later, and NODE_OPTIONS=--experimental-vm-modules. If your imports end in .js, map them in the Jest config:

package.json
{
"jest": {
"moduleNameMapper": { "^(\\.{1,2}/.*)\\.js$": "$1" }
}
}

@runwisp/js is an ES module. Projects that compile to CommonJS, the default before Nest 12, load it with require(). In tsconfig.json, use "module": "commonjs" with TypeScript 5, or "module": "nodenext" with TypeScript 5.8 or later. "module": "node16" can’t load it.

Bun runs the app from source, decorators and injection included:

Terminal window
bun run src/main.ts

To keep using the Nest CLI, tell it to start the app with Bun:

Terminal window
bun --bun x nest start --exec bun

Use RUNWISP_JS=off bun test for tests.