Skip to main content

Background Jobs

Not everything a service does happens in response to a request. Metrics need collecting on an interval, stale records need cleaning up, a digest email needs sending once a day, none of that belongs inside a route handler. RapidREST's built-in scheduler handles exactly this: code that runs outside the request/response cycle, on a schedule or once at startup. As with routes and models, there's no central place jobs get registered, extend the base class, export it, and RapidREST takes care of the rest.

BackgroundService

Every background job extends this abstract base class:

export abstract class BackgroundService {
public abstract get schedule(): string | undefined;
public abstract run(): Promise<void> | void;
public abstract start(): Promise<void> | void;
public abstract stop(): Promise<void> | void;
}
MemberWhat it's for
scheduleA cron expression (via node-schedule) describing how often run() fires. Returning undefined means "run once" (see below).
run()The work itself. Called on every scheduled tick, or exactly once, for an undefined-schedule service.
start()Called once, before scheduling begins. Set up any state run() will need.
stop()Called on shutdown (or immediately after a one-time run() completes). Release whatever start() acquired.

A real background service, adapted from the Petstore example, collecting Prometheus metrics once a minute:

import {BackgroundService} from '@rapidrest/service-core';
import * as prom from 'prom-client';

export default class MetricsCollector extends BackgroundService {
private registry: prom.Registry;

constructor() {
super();
this.registry = prom.register;
}

public get schedule(): string | undefined {
return '* * * * *'; // every minute
}

public run(): void {
// collect and record metrics into this.registry
}

public async start(): Promise<void> {
// no setup needed here
}

public async stop(): Promise<void> {
// no cleanup needed here
}
}

Injecting config, a logger, or anything else

BackgroundService doesn't hand you config or logger automatically. A job is a normal dependency-injected class, so pull in whatever it needs the usual way (see Dependency Injection):

import {BackgroundService} from '@rapidrest/service-core';
import {ObjectDecorators} from '@rapidrest/core';
const {Config, Logger} = ObjectDecorators;

export default class CleanupJob extends BackgroundService {
@Config('cleanup:schedule', '0 * * * *')
private cronSchedule!: string;

@Logger
private logger: any;

public get schedule(): string | undefined {
return this.cronSchedule;
}

public run(): void {
this.logger.info('Running cleanup...');
}

public async start(): Promise<void> {}
public async stop(): Promise<void> {}
}

Scheduled vs. run-once services

Whether a service runs on a schedule or just once at startup depends entirely on what its schedule getter returns:

  • Returns a cron string (e.g. "*/5 * * * *" for every 5 minutes): the service is scheduled with node-schedule, and run() fires on every tick for the lifetime of the server.
  • Returns undefined: run() fires exactly once, immediately after start(), and the service is then stopped and torn down automatically. Use this for one-time startup work rather than a recurring job.

If run() throws

A failure inside run() doesn't crash the server. It's caught, logged (Background service '<name>' failed during a scheduled run.), and the service stays scheduled, the next tick still fires normally. A slow run() that's still executing when its next tick comes due doesn't overlap with itself either, the manager skips that tick with a warning rather than letting two invocations of the same service run concurrently. Both of these you get for free, with nothing to write yourself.

What isn't automatic: shutdown doesn't wait for an in-flight run() to finish. stop() is called immediately once the server starts shutting down, racing against whatever run() is still doing rather than waiting for it. If a job does something that would be unsafe to interrupt partway through, coordinating that is on the job itself, check for an in-progress operation in stop(), or keep individual run() calls short enough that this doesn't matter in practice.

Auto-discovery and startup

Background services are found the same way everything else in RapidREST is, see Auto-Discovery. At startup, the server scans every exported class under src/ and hands any class whose prototype chain includes BackgroundService to a BackgroundServiceManager, which instantiates it (running the usual @Inject/@Config dependency injection first), calls start(), and either schedules run() on the configured cron expression or runs it once and stops the service. Services start up one at a time, in discovery order, not all at once, each one's start() completes before the next service's does.

There's no manual registration step. Export a class extending BackgroundService anywhere under src/, and it's live on the next server start.

Controlling services manually

Most of the time, BackgroundServiceManager handles everything for you automatically. When you do need direct control, triggering a job on demand from an admin endpoint, say, inject the manager:

import {ObjectDecorators} from '@rapidrest/core';
import {BackgroundServiceManager} from '@rapidrest/service-core';
const {Inject} = ObjectDecorators;

export class AdminRoute {
@Inject(BackgroundServiceManager) private serviceManager?: BackgroundServiceManager;

async triggerMetricsCollection() {
await this.serviceManager?.start('MetricsCollector');
}
}
MethodWhat it does
start(name) / stop(name)Start or stop one named service
startAll() / stopAll()Start or stop every discovered service, this is what the server itself calls at startup and during shutdown
getService(name)Returns the running instance of a named service, or undefined

Scaffolding a job with the CLI

rapidrest generate job MetricsCollector --schedule "*/5 * * * *"

This generates a BackgroundService subclass under src/jobs/ with the schedule getter pre-filled from --schedule, plus a matching test file. See CLI → Generate Commands for the full flag list.