background tasks illustration

Sending bulk emails, processing a large upload, generating a report. Anything slow enough that holding the request open for it is the wrong answer. These operations can slow down our applications and degrade the user experience if not handled correctly. That’s where task queues come in handy.

NestJS, a Node.js framework for building efficient and scalable server-side applications, has a great solution for this problem with its built-in task scheduling and queue management feature. Using libraries like Bull, it becomes straightforward to set up and manage task queues in a NestJS application.

Let’s start by adding the necessary packages:

npm install --save @nestjs/bull bull

Next, we need to import the BullModule into our application:

import { BullModule } from '@nestjs/bull';

@Module({
  imports: [
    BullModule.forRoot({
      redis: {
        host: 'localhost',
        port: 6379,
      },
    }),
  ],
})
export class AppModule {}

In this configuration, we’re connecting to a local Redis server, which Bull uses as a message broker for managing jobs and ensuring they are processed even in cases of server crashes or restarts.

Next, let’s define a queue:

import { BullModule } from '@nestjs/bull';

@Module({
  imports: [
    BullModule.registerQueue({
      name: 'notifications',
    }),
  ],
})
export class AppModule {}

Now, let’s create a processor for our queue:

import { Processor, Process } from '@nestjs/bull';
import { Job } from 'bull';

@Processor('notifications')
export class NotificationsProcessor {
  @Process()
  async sendNotification(job: Job<unknown>) {
    const { data } = job;
    // your logic to send notifications here
  }
}

The Processor decorator tells NestJS that this class will handle jobs from the ’notifications’ queue. The Process decorator is used to define a job handler.

Finally, let’s add jobs to our queue:

import { InjectQueue } from '@nestjs/bull';
import { Queue } from 'bull';

export class AppService {
  constructor(@InjectQueue('notifications') private notificationsQueue: Queue) {}

  async addNotification(data: unknown) {
    await this.notificationsQueue.add(data);
  }
}

In this example, whenever the addNotification method is called, a new job will be added to the ’notifications’ queue.

The part that’s easy to skip is what happens when a job fails. A queue you haven’t configured retries and a dead-letter destination for isn’t resilience, it’s just a slower way to lose the work silently.