Data Polling on the Backend for Long-Running HTTP Requests: NestJS Example
Yevheniia
Posted on November 16, 2024
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)
💖 💪 🙅 🚩
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
polling Data Polling on the Backend for Long-Running HTTP Requests: NestJS Example
November 16, 2024