Data Polling on the Backend for Long-Running HTTP Requests: NestJS Example

yevheniia_br

Yevheniia

Posted on November 16, 2024

Data Polling on the Backend for Long-Running HTTP Requests: NestJS Example

Following my previous article on long-runnig http requests handling on the frontend, I’d like to demonstrate how to implement data polling on the backend. This example uses a NestJS server app, PostgreSQL database, and Prisma ORM. However, this approach is universal and can be applied with any other programming language, framework, or database.

Here’s the workflow:

  1. A /purchase/execute HTTP request is received.
  2. A new taskId is generated, and a new entity is created in the tasks table of the database.
  3. The method responsible for purchase execution is called in the background with taskId as an argument (without waiting for its result).
  4. The server returns an HTTP 202 status and the taskId to the client.
  5. Once the purchase execution method finishes, the result is stored in the database.
  6. The client polls/purchase/execution-status/${taskId} until the status is "done", at which point the response is returned.
  7. The database is cleaned up automatically using TTL (e.g., AWS RDS with expireAt). Alternatively, you can use a Cron job to periodically remove expired tasks.

Here’s how this workflow can be implemented in NestJS:

//purchase.controller.ts

  import {
  Controller,
  Post,
  UseGuards,
  Body,
  Get,
  Param,
  Res,
  HttpStatus,
} from '@nestjs/common';
import { ApiBearerAuth } from '@nestjs/swagger';
import { Response } from 'express';

import jsend from 'jsend';
import { AuthenticatedGuard } from '../auth/auth.guard';
import { PurchaseService } from './purchase.service';
import { TasksService } from '../tasks/tasks.service';

@Controller('purchase')
export class PurchaseController {
  constructor(
    private readonly purchaseService: PurchaseService,
    private readonly tasksService: TasksService,
  ) {}

  @UseGuards(AuthenticatedGuard)
  @ApiBearerAuth()
  @Post('execute')
  public async executePurchase(
    @Body()
    executePurchaseDto: {
      userId: string;
      goods: Array<{
        productId: string;
        quantity: number;
      }>;
    },
    @Res() res: Response,
  ) {
    const taskId =
      await this.tasksService.createTask();
    this.purchaseService.executePurchase(
      executePurchaseDto,
      taskId,
    );

    res
      .status(HttpStatus.ACCEPTED)
      .send({
        taskId,
        status: HttpStatus.ACCEPTED,
      });
  }

  @UseGuards(AuthenticatedGuard)
  @ApiBearerAuth()
  @Get('execution-status/:taskId')
  public async getPurchaseExecutionStatus(
    @Param('taskId') taskId: string,
    @Res() res: Response,
  ) {
    const result =
      await this.purchaseService.getPurchaseExecutionStatus(
        taskId,
      );

    return res
      .status(HttpStatus.OK)
      .json(jsend.success(result));
  }
}


Enter fullscreen mode Exit fullscreen mode
//purchase.service.ts

import { Injectable } from '@nestjs/common';
import { Task } from '@prisma/client';
import { TasksService } from '../tasks/tasks.service';

@Injectable()
export class PurchaseService {
  constructor(
    private readonly tasksService: TasksService,
  ) {}

  public async executePurchase(
    executePurchaseDto: {
      userId: string;
      goods: Array<{
        productId: string;
        quantity: number;
      }>;
    },
    taskId: string,
  ): Promise<any> {
    const res = await this.processPurchase(
      executePurchaseDto,
      taskId,
    );
    await this.tasksService.updateTaskById(res);
  }

  public async getPurchaseExecutionStatus(
    taskId: string,
  ): Promise<Task> {
    return await this.tasksService.getTaskById(
      taskId,
    );
  }

  public async processPurchase(
    executePurchaseDto: {
      userId: string;
      goods: Array<{
        productId: string;
        quantity: number;
      }>;
    },
    taskId: string,
  ): Promise<any> {
    // Your purchase handling logic here
    const result = {};

    return { result, taskId };
  }
}

Enter fullscreen mode Exit fullscreen mode
//tasks.service.ts
import { Injectable } from '@nestjs/common';
import { v4 as uuidv4 } from 'uuid';
import { Task } from '@prisma/client';

import { PrismaService } from 'src/prisma/prisma.service';

@Injectable()
export class TasksService {
  constructor(
    private prismaService: PrismaService,
  ) {}

  public async createTask(): Promise<string> {
    const currentDate = Date.now();
    const createdAt = Math.floor(
      currentDate / 1000,
    ); // in seconds
    const expireAt = createdAt + 900; // + 15 minutes

    const params = {
      taskId: uuidv4(),
      createdAt: new Date(
        currentDate,
      ).toISOString(),
      status: 'processing',
      response: '-',
      expireAt,
    };

    const newTask =
      await this.prismaService.task.create({
        data: {
          ...params,
        },
      });

    if (!newTask) {
      throw new HttpException(
        'Failed to create a task',
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }

    return params.taskId;
  }

  public async getTaskById(
    taskId: string,
  ): Promise<Task> {
    const task =
      await this.prismaService.task.findUnique({
        where: {
          taskId,
        },
      });
    if (!task)
      throw new HttpException(
        "Task doesn't exist",
        HttpStatus.BAD_REQUEST,
      );

    return task;
  }

  public async updateTaskById({
    taskId,
    result,
  }): Promise<void> {
    const updatedTask =
      await this.prismaService.task.update({
        where: { taskId },
        data: {
          response: result,
          status: 'done',
        },
      });
    if (!updatedTask) {
      throw new HttpException(
        'Failed to update task',
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }
  }
}


Enter fullscreen mode Exit fullscreen mode
//prisma.service.ts

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient {
  constructor(
    public configService: ConfigService,
  ) {
    super({
      datasources: {
        db: {
          url: configService.get('DATABASE_URL'),
        },
      },
    });
  }
}


Enter fullscreen mode Exit fullscreen mode

Thanks for reading and feel free to shair your feedback)

💖 💪 🙅 🚩
yevheniia_br
Yevheniia

Posted on November 16, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related