Не всё происходит внутри запроса. Иногда провайдер нужно создать асинхронно ещё до старта приложения, иногда задача должна выполняться по расписанию, иногда тяжёлую работу нужно вынести из запроса в фон. NestJS даёт инструменты для каждого из этих случаев.
Когда провайдер нельзя создать сразу
Обычно NestJS создаёт провайдеры синхронно при старте. Но что если нужно сначала дождаться подключения к Redis или загрузить конфигурацию из удалённого источника?
Для этого есть async-провайдеры — фабрика с ключевым словом async:
{
provide: 'CACHE',
useFactory: async (config: ConfigService) => {
const client = createClient({ url: config.getOrThrow('REDIS_URL') });
await client.connect();
return client;
},
inject: [ConfigService],
}
NestJS дождётся завершения фабрики, прежде чем поднять зависящие от неё компоненты. Приложение не начнёт принимать запросы, пока все async-провайдеры не будут готовы. Закрытие такого соединения при остановке вешают на lifecycle-хук onModuleDestroy — подробнее в статье конфигурация и жизненный цикл.
Задачи по расписанию
Бывают задачи, которые должны выполняться периодически: отчёт раз в сутки, очистка устаревших записей раз в час, синхронизация данных каждые пять минут.
Для этого есть пакет @nestjs/schedule. Подключается в модуле:
import { ScheduleModule } from '@nestjs/schedule';
@Module({
imports: [ScheduleModule.forRoot()],
})
export class AppModule {}
После этого на метод любого провайдера можно повесить декоратор @Cron с выражением в формате cron, или @Interval с интервалом в миллисекундах:
import { Injectable } from '@nestjs/common';
import { Cron, Interval } from '@nestjs/schedule';
@Injectable()
export class ReportTasks {
@Cron('0 3 * * *') // каждый день в 03:00
async buildDailyReport() {
// формируем отчёт
}
@Interval(5 * 60 * 1000) // каждые 5 минут
async syncExternalData() {
// синхронизация
}
}
Ловушка с несколькими экземплярами
Если приложение запущено в нескольких копиях (несколько контейнеров), @Cron сработает в каждой из них. Задача выполнится столько раз, сколько реплик. Для вспомогательных задач это часто терпимо, но для важных операций — нет.
Решение: расписание выносят наружу — в планировщик оркестратора (Kubernetes CronJob), внешний планировщик или выделенный сервис. Тогда расписание живёт в одном месте независимо от числа реплик приложения.
Очереди для надёжного фона
Когда фоновая работа важна — её нельзя потерять, она тяжёлая или должна повторяться при сбое — используют очередь. Стандартный выбор для NestJS — BullMQ через пакет @nestjs/bullmq (работает поверх Redis).
Принцип простой: сервис кладёт задачу в очередь и сразу отвечает клиенту. Отдельный обработчик (воркер) забирает задачу, выполняет её, а при ошибке — повторяет по настроенной политике.
Постановка задачи:
import { Injectable } from '@nestjs/common';
import { InjectQueue } from '@nestjs/bullmq';
import { Queue } from 'bullmq';
@Injectable()
export class ImportService {
constructor(@InjectQueue('imports') private readonly queue: Queue) {}
async scheduleImport(fileId: string): Promise<void> {
await this.queue.add('process-import', { fileId });
}
}
Контроллер, вызвавший scheduleImport, отвечает сразу — 202 Accepted («принято в обработку»), не дожидаясь результата. Обработчик задачи — отдельный класс с декоратором @Processor:
import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Job } from 'bullmq';
@Processor('imports')
export class ImportProcessor extends WorkerHost {
async process(job: Job): Promise<void> {
const { fileId } = job.data;
// обрабатываем файл
}
}
Очередь переживёт перезапуск приложения: задачи хранятся в Redis и не теряются.
Когда что выбирать
- Async-провайдер — когда соединение или ресурс нужно инициализировать асинхронно до старта приложения.
@Cron/@Interval— для простых периодических задач в однократно запущенном приложении. Убедитесь, что дублирование при нескольких репликах не создаёт проблем.- Очередь (BullMQ) — для важной фоновой работы, которую нельзя потерять: тяжёлые операции, обработка файлов, отправка уведомлений, интеграции с внешними системами.
Правило простое: лёгкое и некритичное можно делать в фоне процесса; важное, тяжёлое или требующее повторных попыток — в очередь.
Коротко
- Async-провайдеры позволяют инициализировать соединения асинхронно — NestJS дождётся завершения фабрики до подъёма зависимостей.
@nestjs/scheduleдобавляет@Cronи@Intervalдля периодических задач; при нескольких репликах задача выполняется в каждой — это нужно учитывать.- BullMQ (
@nestjs/bullmq) — надёжный способ выносить тяжёлую работу в фон: задачи хранятся в Redis, переживают перезапуск, повторяются при ошибке. - Контроллер, поставив задачу в очередь, отвечает
202 Accepted— он не ждёт результата. - Важная фоновая работа живёт в очереди, а не «прячется» вдогонку после ответа.
Что почитать дальше
- Конфигурация и жизненный цикл — lifecycle-хуки и graceful shutdown.
- Наблюдаемость — как следить за фоновыми задачами и очередями.
- Модули и DI — как устроены провайдеры и инъекция зависимостей в NestJS.