
Daniel Ortega
Software Engineer
NestJS: HttpService retry configuration

Well, I'm here to explain in a simple post how we can configure retries in an API call using the Http Module package of NestJS.
To learn more about the module, you can refer to the official NestJS documentation, but all we really need to know is that it is essentially a wrapper for Axios.
So knowing that, initial step would be to install the necessary dependencies:
Therefore, we will update this same method so that, if we encounter an internal API error, we'll attempt to retry the request.
In this scenario, we'll retry whenever the response status is ≥ 500. To achieve this, I'll create my own error, which will indicate when I should retry the request based on the status code of the error.
First of all, it's important to know that the return value of the HttpService methods (GET, POST, etc.) is an observable, so we can leverage rxjs.
We will use pipe to chain these operators in a more readable and composable way.
Within this chain, we will use map to filter the values of the response.
Now, it's important to note that there is a retry option, which allows us to configure the way retry attempts are made.
const response = this.httpService
.get('http://localhost:3000/cars')
.pipe(
map((response) => {
if (response.status === HttpStatus.OK) {
return response.data;
}
throw new CarsApiError(response.message, response.status);
})
catchError((error: Error) => {
this.logger.error(error);
throw error;
}),
retry({
count: retryCount,
delay: (error: Error, retryCount: number) => {
if (error instanceof CarsApiError
&& error.status >= HttpStatus.INTERNAL_SERVER_ERROR) {
this.logger.warn(
`Retrying GET Cars endpoint. Retry: ${retryCount}`,
);
return timer(retryDelay);
}
throw error;
},
}),
...
The first parameter (count) allows us to specify the number of times we want to retry our request.
The second parameter (delay) enables us to pass a function where we receive the error as a function param and the current retry number.
In our case, as mentioned earlier, we only want to retry those errors thrown by us previously when the API responds with a status ≥ 500. Therefore, once we create our condition, if everything is met, we return a timer (a timer is used to generate values at defined time intervals) with a value we find appropriate.
In this scenario, both the values for retryCount and retryDelay are obtained before making our request, relying on the ConfigService of NestJS, which allows us to access those values previously configured in the application's environment.
Now that we know we will get an Observable, to obtain the information in the form of a Promise, we can use lastValueFrom and get the last emitted value to work with. In this case, we need to wait for the value to construct our Entity that we will work with internally and that we must return.
To learn more about the module, you can refer to the official NestJS documentation, but all we really need to know is that it is essentially a wrapper for Axios.
So knowing that, initial step would be to install the necessary dependencies:
$npm i --save @nestjs/axios axios
The next step would be to import our module wherever we want to use it. For instance, we could install it in our cars module: @Module({
controllers: [CarsController],
imports: [PrismaModule, HttpModule],
providers: [
{
provide: 'ICarsRepository',
useClass: CarsRepository,
}
CarsService,
Logger,
],
exports: [CarsService, CarsRepository],
}
export class CarsModule {}
Once we have it imported into our module, we'll be able to inject it as a dependency through the constructor in any class within our "cars" module, for example, inside the CarsService.controllers: [CarsController],
imports: [PrismaModule, HttpModule],
providers: [
{
provide: 'ICarsRepository',
useClass: CarsRepository,
}
CarsService,
Logger,
],
exports: [CarsService, CarsRepository],
}
export class CarsModule {}
@Injectable()
export class CarService {
constructor(private readonly httpService: HttpService) {}
public findAll(): Observable<AxiosResponse<any>> {
return this.httpService.get('http://localhost:3000/cars');
}
}
As we can see, we've made a rather simple GET call and haven't configured anything in this case. export class CarService {
constructor(private readonly httpService: HttpService) {}
public findAll(): Observable<AxiosResponse<any>> {
return this.httpService.get('http://localhost:3000/cars');
}
}
Therefore, we will update this same method so that, if we encounter an internal API error, we'll attempt to retry the request.
In this scenario, we'll retry whenever the response status is ≥ 500. To achieve this, I'll create my own error, which will indicate when I should retry the request based on the status code of the error.
class CarsApiError extends Error {
constructor(message, status) {
super(message);
this.status = status;
}
}
Now, all that's left is to configure our call. constructor(message, status) {
super(message);
this.status = status;
}
}
First of all, it's important to know that the return value of the HttpService methods (GET, POST, etc.) is an observable, so we can leverage rxjs.
We will use pipe to chain these operators in a more readable and composable way.
Within this chain, we will use map to filter the values of the response.
...
const response = this.httpService
.get('http://localhost:3000/cars')
.pipe(
map((response) => {
if (response.status === HttpStatus.OK) {
return response.data;
}
throw new CarsApiError(response.message, response.status);
})
catchError((error: Error) => {
this.logger.error(error);
throw error;
}),
...
As we can observe, if the API response is not OK (200), we will throw an instance of the previously created error with the status of the response. Similarly, we will have a catch block for any potential errors not handled by the API and to log any errors in our logs. const response = this.httpService
.get('http://localhost:3000/cars')
.pipe(
map((response) => {
if (response.status === HttpStatus.OK) {
return response.data;
}
throw new CarsApiError(response.message, response.status);
})
catchError((error: Error) => {
this.logger.error(error);
throw error;
}),
...
Now, it's important to note that there is a retry option, which allows us to configure the way retry attempts are made.
const response = this.httpService
.get('http://localhost:3000/cars')
.pipe(
map((response) => {
if (response.status === HttpStatus.OK) {
return response.data;
}
throw new CarsApiError(response.message, response.status);
})
catchError((error: Error) => {
this.logger.error(error);
throw error;
}),
retry({
count: retryCount,
delay: (error: Error, retryCount: number) => {
if (error instanceof CarsApiError
&& error.status >= HttpStatus.INTERNAL_SERVER_ERROR) {
this.logger.warn(
`Retrying GET Cars endpoint. Retry: ${retryCount}`,
);
return timer(retryDelay);
}
throw error;
},
}),
...
The second parameter (delay) enables us to pass a function where we receive the error as a function param and the current retry number.
In our case, as mentioned earlier, we only want to retry those errors thrown by us previously when the API responds with a status ≥ 500. Therefore, once we create our condition, if everything is met, we return a timer (a timer is used to generate values at defined time intervals) with a value we find appropriate.
In this scenario, both the values for retryCount and retryDelay are obtained before making our request, relying on the ConfigService of NestJS, which allows us to access those values previously configured in the application's environment.
const retryCount = parseInt(this.config.get('HTTP_SERVICE_RETRY_COUNT'));
const retryDelay = parseInt(this.config.get('HTTP_SERVICE_RETRY_DELAY'));
If it doesn't meet our conditions, it means that the error is not one we want to retry, so we throw the error again, and the execution continues. const retryDelay = parseInt(this.config.get('HTTP_SERVICE_RETRY_DELAY'));
Now that we know we will get an Observable, to obtain the information in the form of a Promise, we can use lastValueFrom and get the last emitted value to work with. In this case, we need to wait for the value to construct our Entity that we will work with internally and that we must return.
const carValues = await lastValueFrom(response);
return Car.toEntity(... carValues);
Finally, our complete code should look something like the following: return Car.toEntity(... carValues);
@Injectable()
export class CarsService {
constructor(
private readonly httpService: HttpService,
private readonly config: ConfigService
private readonly logger: Logger,
) {}
public async findAll(): Promise<Car> {
const retryCount = parseInt(this.config.get('HTTP_SERVICE_RETRY_COUNT'));
const retryDelay = parseInt(this.config.get('HTTP_SERVICE_RETRY_DELAY'));
const response = this.httpService
.get('http://localhost:3000/cars')
.pipe(
map((response) => {
if (response.status === HttpStatus.OK) {
return response.data;
}
throw new CarsApiError(response.message, response.status);
})
catchError((error: Error) => {
this.logger.error(error);
throw error;
}),
retry({
count: retryCount,
delay: (error: Error, retryCount: number) => {
if (error instanceof CarsApiError
&& error.status >= HttpStatus.INTERNAL_SERVER_ERROR) {
this.logger.warn(
`Retrying GET Cars endpoint. Retry: ${retryCount}`,
);
return timer(retryDelay);
}
throw error;
},
}),
);
const carValues = await lastValueFrom(response);
return Car.toEntity(... carValues);
}
}
I hope you find this post helpful and that it helps you to understand how to configure retries in an API call using the Http Module package of NestJS.export class CarsService {
constructor(
private readonly httpService: HttpService,
private readonly config: ConfigService
private readonly logger: Logger,
) {}
public async findAll(): Promise<Car> {
const retryCount = parseInt(this.config.get('HTTP_SERVICE_RETRY_COUNT'));
const retryDelay = parseInt(this.config.get('HTTP_SERVICE_RETRY_DELAY'));
const response = this.httpService
.get('http://localhost:3000/cars')
.pipe(
map((response) => {
if (response.status === HttpStatus.OK) {
return response.data;
}
throw new CarsApiError(response.message, response.status);
})
catchError((error: Error) => {
this.logger.error(error);
throw error;
}),
retry({
count: retryCount,
delay: (error: Error, retryCount: number) => {
if (error instanceof CarsApiError
&& error.status >= HttpStatus.INTERNAL_SERVER_ERROR) {
this.logger.warn(
`Retrying GET Cars endpoint. Retry: ${retryCount}`,
);
return timer(retryDelay);
}
throw error;
},
}),
);
const carValues = await lastValueFrom(response);
return Car.toEntity(... carValues);
}
}