DEV Community

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

Posted on • Edited on

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)

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

Top comments (2)

Collapse
 
kostyatretyak profile image
Костя Третяк • Edited

To add TypeScript syntax highlighting to Markdown, you can use the following syntax (note the ts):

```ts
// Your code here
```
Enter fullscreen mode Exit fullscreen mode
Collapse
 
yevheniia_br profile image
Yevheniia

Thanks, Kostya, looks much better))

AWS Security LIVE!

Tune in for AWS Security LIVE!

Join AWS Security LIVE! for expert insights and actionable tips to protect your organization and keep security teams prepared.

Learn More

👋 Kindness is contagious

Dive into an ocean of knowledge with this thought-provoking post, revered deeply within the supportive DEV Community. Developers of all levels are welcome to join and enhance our collective intelligence.

Saying a simple "thank you" can brighten someone's day. Share your gratitude in the comments below!

On DEV, sharing ideas eases our path and fortifies our community connections. Found this helpful? Sending a quick thanks to the author can be profoundly valued.

Okay