bull-board: V3 not working with NestJS

I’m running into an issue where is appears the serverAdapter isn’t handling req as expected — I just simply get a 404. I setup a basic example Stackblitz => https://stackblitz.com/edit/nestjs-starter-demo-gvfxkh

I was using v1.5.1 before - here is was my working main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { router } from 'bull-board';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use('/admin/queues', router);
  await app.listen(3000);
}
bootstrap();

I migrated to v3.3.0 yesterday and have no very little luck with getting ExpressAdapter working correctly. Here is my current non-working main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ExpressAdapter } from '@bull-board/express';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const serverAdapter = new ExpressAdapter();
  serverAdapter.setBasePath('/admin/queues');
  app.use('/admin/queues', serverAdapter.getRouter());  

  await app.listen(3000);
}
bootstrap();

Any help would be awesome!

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Comments: 33 (21 by maintainers)

Most upvoted comments

@Module({
  imports: [
    BullModule.registerQueue({
      name: 'queue1',
    }),
  ],
})
export class BullBoardModule implements NestModule {
  @Inject(getQueueToken('queue1'))
  private readonly queue: Queue

  configure(consumer: MiddlewareConsumer) {
    const serverAdapter = new ExpressAdapter()
    const { addQueue, removeQueue, setQueues, replaceQueues } = createBullBoard(
      { queues: [new BullAdapter(this.queue)], serverAdapter },
    )
    serverAdapter.setBasePath('/api/admin/queues')
    consumer.apply(serverAdapter.getRouter()).forRoutes('/admin/queues')
  }
}

This should work too.

I couldn’t get the above examples to work with my setup, so I thought I’d paste what I got to work in case anyone else is in the same boat.

My versions:

$ egrep "nestjs|bull|bull-board" package.json 
    "@bull-board/api": "^3.7.0",
    "@bull-board/express": "^3.7.0",
    "@nestjs/bull": "^0.4.2",
    "@nestjs/common": "^8.1.2",
    "@nestjs/config": "^1.0.2",
    "@nestjs/core": "^8.1.2",
    "@nestjs/event-emitter": "^1.0.0",
    "@nestjs/jwt": "^8.0.0",
    "@nestjs/passport": "^8.0.1",
    "@nestjs/platform-express": "^8.1.2",
    "@nestjs/typeorm": "^8.0.2",
    "bull": "^3.3",
    "bullmq": "^1.51.1",
    "@nestjs/cli": "^8.1.4",
    "@nestjs/schematics": "^8.0.4",
    "@nestjs/testing": "^8.1.2",
    "@types/bull": "^3.15.1",

My Controller:

import { AuScope } from '@au/dtos';
import {
  Request,
  Response,
  All,
  Controller,
  Next,
  UseGuards,
  BadRequestException,
} from '@nestjs/common';
import express from 'express';
import { AuthGuard } from '../auth/auth.guard';
import { Scope } from '../auth/scope.decorator';
import { QueuesManagerService } from './queues-manager.service';

export const bullBoardPath = 'api/queues/admin';

@Controller(bullBoardPath)
@Scope(AuScope.QueuesAdmin)
@UseGuards(AuthGuard)
export class BullBoardController {
  constructor(private readonly service: QueuesManagerService) {}

  @All('*')
  admin(
    @Request() req: express.Request,
    @Response() res: express.Response,
    @Next() next: express.NextFunction,
  ) {
    const router = this.service.router;
    if (!router) {
      throw new BadRequestException('router not ready'); // Shouldn't happen.
    }

    const entryPointPath = '/' + bullBoardPath + '/';
    req.url = req.url.replace(entryPointPath, '/');

    router(req, res, next);
  }
}

And the relevant part of QueuesManagerService, which initializes bull-board. Note that initBullBoard is called as part of onModuleInit:

  readonly initBullBoard = () => {
    const funcPrefix = `${prefix}.initBullBoard: `;
    this.logger.log(`${funcPrefix}registering queues with UI`);
    const queues = this.queues.map((_) => new BullAdapter(_));

    const serverAdapter = new ExpressAdapter();
    const basePath = '/' + bullBoardPath;
    serverAdapter.setBasePath(basePath);

    createBullBoard({
      queues,
      serverAdapter,
    });

    this.router = serverAdapter.getRouter() as express.Express;
    this.logger.log(`${funcPrefix}registered queues with UI`);
  };

The odd thing is that serverAdapter is told the base path, and prefixes all the client requests with the base path, as appropriate. However, the express router that it returns does not use the given prefix to set its routes. Without the req.url = req.url.replace(entryPointPath, '/'); line, I was getting 404s since the router didn’t match the path - it needs ‘/’ instead of ‘/api/queues/admin/’.

@felixmosh finally got some time to flush this out, let me know what you think.

  • Created a class to act as a pool for my queues
import { BullAdapter } from '@bull-board/api/bullAdapter';
import { BaseAdapter } from '@bull-board/api/dist/src/queueAdapters/base';
import { Injectable } from '@nestjs/common';
import { Queue } from 'bull';

@Injectable()
export class BullBoardQueue { }

export const queuePool: Set<Queue> = new Set<Queue>();

export const getBullBoardQueues = (): BaseAdapter[] => {
    const bullBoardQueues = [...queuePool].reduce((acc: BaseAdapter[], val) => {
        acc.push(new BullAdapter(val))
        return acc
    }, []);

    return bullBoardQueues
}
  • In my main.ts I get my queue pool and loop over it and call addQueue
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ExpressAdapter } from '@bull-board/express';
import { createBullBoard } from '@bull-board/api';
import { getBullBoardQueues } from './bull-board-queue';
import { BaseAdapter } from '@bull-board/api/dist/src/queueAdapters/base';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const serverAdapter = new ExpressAdapter();
    const queues = getBullBoardQueues();

    serverAdapter.setBasePath('/admin/queues');
    app.use('/admin/queues', serverAdapter.getRouter());

    const { addQueue } = createBullBoard({
        queues: [],
        serverAdapter
    });

    queues.forEach((queue: BaseAdapter) => {
        addQueue(queue);
    });

  await app.listen(3000);
}
bootstrap();

I just import my class and add my queue pool => queuePool.add(updateQueue) — not perfect but gets the job done. Thanks for the help!

I used @asomethings’s solution and added a BasicAuth middleware. My goal was to have everything related to queues in a single module and not spread out in main/bootstrap.ts.

import {
  DynamicModule,
  MiddlewareConsumer,
  Module,
  NestMiddleware,
  NestModule,
} from '@nestjs/common';
import { BullModule, InjectQueue } from '@nestjs/bullmq';
import { Queue } from 'bullmq';
import { createBullBoard } from '@bull-board/api';
import { BullAdapter } from '@bull-board/api/bullAdapter';
import { ExpressAdapter } from '@bull-board/express';

import { TestProcessor } from './test.processor';
import { NextFunction, Request, Response } from 'express';

class BasicAuthMiddleware implements NestMiddleware {
  private readonly username = 'user';
  private readonly password = 'password';
  private readonly encodedCreds = Buffer.from(
    this.username + ':' + this.password,
  ).toString('base64');

  use(req: Request, res: Response, next: NextFunction) {
    const reqCreds = req.get('authorization')?.split('Basic ')?.[1] ?? null;

    if (!reqCreds || reqCreds !== this.encodedCreds) {
      res.setHeader(
        'WWW-Authenticate',
        'Basic realm=Yours realm, charset="UTF-8"',
      );
      res.sendStatus(401);
    } else {
      next();
    }
  }
}

@Module({})
export class QueuesModule implements NestModule {
  static register(): DynamicModule {
    const testQueue = BullModule.registerQueue({
      name: 'test',
      defaultJobOptions: {
        attempts: 3,
        backoff: {
          type: 'exponential',
          delay: 1000,
        },
      },
    });

    if (!testQueue.providers || !testQueue.exports) {
      throw new Error('Unable to build queue');
    }

    return {
      module: QueuesModule,
      imports: [
        BullModule.forRoot({
          connection: {
            host: 'localhost',
            port: 15610,
          },
        }),
        testQueue,
      ],
      providers: [TestProcessor, ...testQueue.providers],
      exports: [...testQueue.exports],
    };
  }

  constructor(@InjectQueue('test') readonly queue: Queue) {}

  configure(consumer: MiddlewareConsumer) {
    const serverAdapter = new ExpressAdapter();
    const { addQueue, removeQueue, setQueues, replaceQueues } = createBullBoard(
      { queues: [new BullAdapter(this.queue)], serverAdapter },
    );
    serverAdapter.setBasePath('/queues');
    consumer
      .apply(BasicAuthMiddleware, serverAdapter.getRouter())
      .forRoutes('/queues');
  }
}

This is the reason that I’ve asked, since I’m not familiar with Nest.js ecosystem :]

@DennisSnijder it looks really clean … Few small things that pop to mind

  1. Where there is an access to the bull-board dynamic api (what is returns when you call to createBullBoard)
  2. There is a way to change the underline express.js in nest.js, if so, where you can pass different server adapter?

Thanks for the feedback!

For the first one, i’ll update the example on how to do that! For the second one, at the moment the ExpressAdapter is hardcoded, however… NestJS only supports Express and Fastify, since you also have a Fastify adapter available, i’ll take a look into supporting Fastify. Thanks!

edit: I updated the example repository with a “feature controller” whichs is getting the “BullBoardInstance” injected for usage.

If you have multiple queues and the queue registration is spread throughout your app, you can do something like this: Global BullQueueModule with 1 service:

@Global()
@Module({
  providers: [BullQueueService],
  exports: [BullQueueService],
})
export class BullQueueModule {}

BullQueueService:

import { Injectable, OnModuleInit } from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
import { Queue } from 'bull';

import * as basicAuth from 'express-basic-auth';
import { ExpressAdapter } from '@bull-board/express';
import { createBullBoard } from '@bull-board/api';
import { BullAdapter } from '@bull-board/api/bullAdapter';

@Injectable()
export class BullQueueService implements OnModuleInit {
  private readonly queues = new Set<Queue>();

  constructor(private readonly adapterHost: HttpAdapterHost) {}

  addQueue(queue: any) {
    this.queues.add(queue);
  }

  onModuleInit() {
    const serverAdapter = new ExpressAdapter();
    serverAdapter.setBasePath('/admin/queues');

    createBullBoard({
      queues: Array.from(this.queues).map((queue) => new BullAdapter(queue)),
      serverAdapter,
    });

    this.adapterHost.httpAdapter.use(
      '/admin/queues',
      serverAdapter.getRouter(),
    );
    this.adapterHost.httpAdapter.use(
      '/admin/queues',
      basicAuth({
        challenge: true,
        users: { user: 'pass' },
      }),
    );
  }
}

Modules that add their queues to bull queue service:

@Module({
  imports: [
    BullModule.registerQueue({
      name: VIDEO_PROCESSOR_QUEUE,
    }),
  ],
  providers: [VideoProcessorService, VideoProcessorConsumer],
  exports: [VideoProcessorService],
})
export class VideoProcessorModule {
  constructor(
    @InjectQueue(VIDEO_PROCESSOR_QUEUE)
    public readonly videoProcessorQueue: Queue,
    public readonly bullQueueService: BullQueueService,
  ) {
    this.bullQueueService.addQueue(this.videoProcessorQueue);
  }
}

@felixmosh finally got some time to flush this out, let me know what you think.

  • Created a class to act as a pool for my queues
import { BullAdapter } from '@bull-board/api/bullAdapter';
import { BaseAdapter } from '@bull-board/api/dist/src/queueAdapters/base';
import { Injectable } from '@nestjs/common';
import { Queue } from 'bull';

@Injectable()
export class BullBoardQueue { }

export const queuePool: Set<Queue> = new Set<Queue>();

export const getBullBoardQueues = (): BaseAdapter[] => {
    const bullBoardQueues = [...queuePool].reduce((acc: BaseAdapter[], val) => {
        acc.push(new BullAdapter(val))
        return acc
    }, []);

    return bullBoardQueues
}
  • In my main.ts I get my queue pool and loop over it and call addQueue
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ExpressAdapter } from '@bull-board/express';
import { createBullBoard } from '@bull-board/api';
import { getBullBoardQueues } from './bull-board-queue';
import { BaseAdapter } from '@bull-board/api/dist/src/queueAdapters/base';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const serverAdapter = new ExpressAdapter();
    const queues = getBullBoardQueues();

    serverAdapter.setBasePath('/admin/queues');
    app.use('/admin/queues', serverAdapter.getRouter());

    const { addQueue } = createBullBoard({
        queues: [],
        serverAdapter
    });

    queues.forEach((queue: BaseAdapter) => {
        addQueue(queue);
    });

  await app.listen(3000);
}
bootstrap();

I just import my class and add my queue pool => queuePool.add(updateQueue) — not perfect but gets the job done. Thanks for the help!

I spend the day trying to ‘share’ addQueue from main.ts to my queueService. A good solution was to use the HttpAdapterHost of my AppModule

@Injectable()
export class QueueService {

	constructor(
		@InjectQueue(QueueEnums.QueueName) private readonly _queue: Queue,
		private adapterHost: HttpAdapterHost
	) {
		const serverAdapter = new ExpressAdapter() 
		serverAdapter.setBasePath('/admin/queues')
		createBullBoard({
			queues: [new BullAdapter(_queue)], 
			serverAdapter: serverAdapter
		})		
		this.adapterHost.httpAdapter.use('/admin/queues', serverAdapter.getRouter())
	}
```