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/execute
HTTP request is received.- A new
taskId
is 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 status
and 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 { 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,
);
}
}
}
//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)
Top comments (0)