When building back-end services with NestJS, making reliable and observable HTTP requests is crucial, especially when dealing with third-party APIs or microservice communication.
In this article, we’ll create a reusable AdvancedAxiosService that wraps Axios with:
- Request timing
- Retry capabilities
- Logging for success, retry, and failure scenarios
Let's get started. 💡
🎯 Why Use a Custom Axios Service?
Axios is a powerful HTTP client, but out of the box, it lacks advanced features like:
- Built-in retries for failed requests
- Logging request metadata such as latency
- Unified interface for all request types
With NestJS, we can wrap Axios into a service that includes all of these features and inject it wherever we need.
📦 Install Dependencies
We’ll use axios-retry, a lightweight library that automatically retries failed Axios requests with customizable logic.
Before diving into the code, make sure you have axios and axios-retry installed in your project:
npm install axios axios-retry
Or with yarn:
yarn add axios axios-retry
🛠️ The Service Implementation
In this section, we’re creating a custom injectable service in NestJS that wraps the Axios HTTP client with enhanced functionality. This includes automatic retries for failed requests using axios-retry, and detailed logging for request duration and outcomes.
import { Injectable, Logger } from '@nestjs/common'
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import axiosRetry from 'axios-retry'
@Injectable()
export class AdvancedAxiosService {
private readonly axiosInstance: AxiosInstance
private readonly logger = new Logger(AdvancedAxiosService.name)
constructor() {
this.axiosInstance = axios.create()
// Track request start time
this.axiosInstance.interceptors.request.use((config) => {
;(config as any).metadata = { startTime: new Date() }
return config
})
// Setup retry logic, see more details of options: https://github.com/softonic/axios-retry?tab=readme-ov-file#options.
axiosRetry(this.axiosInstance, {
retries: 3,
retryDelay: (retryCount) => retryCount * 1500,
shouldResetTimeout: true,
retryCondition: () => true,
onRetry: (retryCount, error, requestConfig) => {
const metadata = (requestConfig as any).metadata
const duration = new Date().getTime() - metadata.startTime.getTime()
this.logger.warn(
`Retry #${retryCount} for request to ${requestConfig.url} failed. Error: ${error.message}. Duration: ${duration}ms`,
)
},
})
// Log response duration
this.axiosInstance.interceptors.response.use(
(response: AxiosResponse) => {
const metadata = (response.config as any).metadata
const duration = new Date().getTime() - metadata.startTime.getTime()
this.logger.log(
`Successful ${response.config.method?.toUpperCase()} request to ${response.config.url}. Status: ${
response.status
}. Duration: ${duration}ms`,
)
return response
},
(error) => {
const metadata = (error.config as any)?.metadata
if (metadata) {
const duration = new Date().getTime() - metadata.startTime.getTime()
this.logger.error(
`Failed ${error.config.method?.toUpperCase()} request to ${error.config.url}. Error: ${
error.message
}. Duration: ${duration}ms`,
)
}
return Promise.reject(error)
},
)
}
async request<T = any>(config: AxiosRequestConfig): Promise<T> {
const response = await this.axiosInstance.request<T>(config)
return response.data
}
async get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
return this.request<T>({ ...config, method: 'GET', url })
}
async post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
return this.request<T>({ ...config, method: 'POST', url, data })
}
}
Here’s a breakdown of what we’re adding:
- Axios instance creation: Instead of using Axios globally, we create an isolated instance so we can apply custom configuration and interceptors without side effects.
- Request interceptor: Captures the start time of each request, so we can later calculate how long it took to complete (latency).
- Retry configuration: Automatically retries failed requests up to 3 times with a 1.5-second incremental delay between each attempt. This is useful for handling temporary network issues or rate limits.
- Response interceptor: Logs successful responses with method, URL, status code, and latency.
- Error handler: Logs failed requests with relevant metadata, including error messages and how long the failed attempt took.
- Helper methods (get, post, request): These wrap Axios request methods and return only the response data for cleaner usage.
This service is designed to be reusable across your project, giving you consistency and better observability for all outbound HTTP calls.
🧪 How to Use It
- Register it in your module:
@Module({
providers: [AdvancedAxiosService],
exports: [AdvancedAxiosService],
})
export class HttpClientModule {}
2. Inject and use it in your services:
@Injectable()
export class ExampleService {
constructor(private readonly http: AdvancedAxiosService) {}
async fetchData() {
const data = await this.http.get('http://jsonplaceholder.typicode.com/posts');
return data;
}
}
📦 Bonus: Customize It Further
Want to go further? Here are some ideas:
- Integrate with your centralized logging service
- Add request queuing or exponential backoff for specific status codes
🧘 Final Thoughts
The AdvancedAxiosService gives your NestJS applications reliable, observable, and extensible HTTP request capabilities. Whether you're calling internal APIs or external services, this service helps you build more robust apps.
Happy coding! 🚀
Top comments (0)