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:
- A
/purchase/executeHTTP request is received.
- A new
taskIdis generated, and a new entity is created in the tasks table of the database.
- The method responsible for purchase execution is called in the background with taskId as an argument (without waiting for its result).
- The server returns an
HTTP 202 statusand the taskId to the client.
- Once the purchase execution method finishes, the result is stored in the database.
- The client polls
/purchase/execution-status/${taskId}until the status is"done", at which point the response is returned.
- 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));
}
}
//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 };
}
}
//tasks.service.ts
import { Injectable } from '@nestjs/common';
import { v4 as uuidv4 } from 'uuid';
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,
};
await this.prismaService.task.create({
data: {
...params,
},
});
return params.taskId;
}
public async updateTaskById({ taskId, result }): Promise<void> {
await this.prismaService.task.update({
where: { taskId },
data: { response: result, status: 'done' },
});
}
}
//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'),
},
},
});
}
}
Thanks for reading and feel free to shair your feedback)