---
# System prepended metadata

title: HTTP Client

---

# HTTP Client

## 🏗 Base HTTP client (`@autofidev/base-http-client`)

### Library Dependencies
- `axios` (HTTP layer)
    - We chose to go with Axios because of its feature rich functionality
        - We compared it with `node-fetch` and `undici` for performance improvements but we didn't see significant difference.

- `axios-cache-interceptor` (Configurable Caching)
  - [Redis Cache Storage](https://axios-cache-interceptor.js.org/#/pages/storages?id=creating-your-own-storage)
    - The `axios-cache-interceptor` library has thorough documentation on how to configure caching and also an example Redis Cache Storage implementation 

- `mollitia` (Resilience)
    - Circuit breaking, timeouts and retries.


### Features

- Cache
    - Configurable Storage (in-memory, Redis, MongoDB, etc) 
    - Default 5 minute ttl
- Resilience
    - retries (default: `3`)
        - exponential back off optional and enabled by default. Otherwise `back off time = timeout` (`1s` default)
    - timeout (default: `1s`)
- Keep alive
    - Removes TCP overhead and improves performance by reusing the same connection for the same endpoint. Sample testing revealed 46% improvement.

### Options
The following options can be passed when **instantiating** a class that implements the `BaseHttpClient`.

| Option              | Required | Description                                                                                                   | Default Value    |
| ------------------- | -------- | ------------------------------------------------------------------------------------------------------------- | ---------------- |
| cacheSettings       | No       | `axios-cache-interceptor` cache settings (`CacheOptions`)                                                                                  | - |
| exponentialBackoff  | No       | Whether to use exponential backoff for retries or not.                                                        | true             |
| keepAlive           | No       | This will allow reusing connections when making subsequent calls to the same host and remove the TCP overhead | true             |
| requestId           | No       | x-request-id header for each sent request, helpful for tracing a call across multiple services.               | Random UUID      |
| defaultParams       | No       | Default Query Params to be used for all requests                                                              | -                |
| logger              | No       | The logger to be used by the API Client                                                                       | -                |
| name                | No       | Name of the API client instance (used for setting mollitia)                                                                       | -                |
| rejectAndRetryAfter | No       | Reject request and retry it after the specified delay in milliseconds                                         | 1000 ms          |
| retryAttempts       | No       | Retry attempts                                                                                                | 3                |


### Code

```javascript
export abstract class BaseHttpClient {

	protected constructor(options: BaseHttpClientOptions) {
		// Keep alive is turned on by default
		this.keepAlive = options.keepAlive ?? true;
		this.httpAgent = new http.Agent({ keepAlive: this.keepAlive });
		this.httpsAgent = new https.Agent({ keepAlive: this.keepAlive });
		this.retryAttempts = options.retryAttempts ?? this.DEFAULT_RETRY_ATTEMPTS;
		this.rejectAndRetryAfter = options.rejectAndRetryAfter ?? this.DEFAULT_REJECT_AND_RETRY_AFTER_IN_MS;
		this.axios = this.prepareAxios(options);
		this.retryWithTimeoutCircuit = this.prepareRetryWithTimeoutCircuit(this.retryAttempts, this.rejectAndRetryAfter);
		this.logger = options.logger;
		this.instanceName = options.name;
	}

	private prepareAxios(options: BaseHttpClientOptions) {
		// Create and config axios client 
	}

	private prepareRetryWithTimeoutCircuit = (retryAttempts: number, rejectAndRetryAfter: number): Circuit => {
		// Creates a Mollitia Retry and Timeout Circuit	
	};

	/**
	 * composeBaseUrl can be used if base url needs to be dynamic.
	 * It will be invoked before each request so use with care.
	 */
	protected composeBaseUrl?(): Promise<string> | string;

	/**
	 * willSendRequest can be used to change the request options dynamically
	 */
	protected willSendRequest?(options: AxiosRequestConfig): Promise<AxiosRequestConfig> | AxiosRequestConfig;

	protected post<TResult = any>(path: string, body?: any, options?: HttpClientRequestConfig) {}

	protected get<TResult = any>(path: string, options?: HttpClientRequestConfig){}

	protected patch<TResult = any>(path: string, body?: any, options?: HttpClientRequestConfig){}

	protected delete<TResult = any>(path: string, body?: any, options?: HttpClientRequestConfig){}

	private async makeRequest<TResult>(options: HttpClientRequestConfig): Promise<CacheAxiosResponse<TResult>> {
		if (!this.composeBaseUrl && !options.baseURL) {
			throw new Error('Expected either baseUrl or composeBaseUrl to be defined');
		}

		let { baseURL } = options;
		if (this.composeBaseUrl && !baseURL) {
			baseURL = await this.composeBaseUrl();
		}

		if (this.willSendRequest) {
			this.axios.interceptors.request.use(this.willSendRequest);
		}

		return this.retryWithTimeoutCircuit.fn(this.axios.request).execute({
			...options,
			headers: {
				...options.headers,
				// Set request id in headers for all requests
				'x-request-id': options?.requestId,
			},
			baseURL: options.baseURL ?? baseURL,
		});
	}
}
```

## 💻 API Client

To integrate with a service, an API Client that extends the `BaseHttpClient` should be created and placed in a new package. The `api-client` nx generator can be used to scaffold a generic API client.

The concerns of this Client will be: 

- **Endpoints**: Exposing the public facing endpoints through different methods.
    
- **Auth**: Setting the correct token in the header(through `willSendRequest`).

- **Types**: Defining the Input and Output types of the service.

### Example: Estimate API Client (`@autofidev/estimate-api-client`)


```javascript
export class EstimateApiClient extends BaseHttpClient {
	constructor(options: Options) {
		super({ ...options, name: 'EstimateApiClient' });
	}

        // used to change the request options dynamically
	protected willSendRequest(options: HttpClientRequestConfig): HttpClientRequestConfig {
		return {
			...options,
			headers: {
				...options.headers,
				Authorization: `Bearer ${createServiceToken()}`,
			},
		};
	}

	public async getOfferMatrix(payload: OfferMatrixInput, options: Options = {}) {
		const { backendPath = BackendPath.Prod, requestId } = options;
		const {
			data: { data },
		} = await this.post<OfferMatrixResponse>('/api/v1/offers', payload, {
			params: { backendPath: backendPath},
			baseURL: process.env.SMARTCOW_ENDPOINT
			requestId,
		});
		return data;
	}
}
```

## ✍️ Design Considerations and Challenges

- Extract shared **API clients** vs Data Sources
- Single package with all API clients vs **one package per client**.
- **Axios** vs Fetch vs Undici.
- base URL **per request** vs on initialization.
- Implementation of the API client by **consumer** vs service team.

### Next Steps 
- ~~Fine tune timeout duration~~ ✅
    - ~~Increase for each retry~~
    - ~~set defaults from data / SLAs~~
- Request ID prefixing when generating within services.
- Override options per request (retries, timeout, etc.).
- Other than REST API Client (GraphQL, async, etc).

## ⚙️ Implementation

### Example: Estimate Subgraph - Smartcow DataSource

```javascript
interface OfferMatrixOptions {
	backendPath?: BackendPath;
	smartCowPr?: number;
}

export class SmartCowSource extends DataSource<ApolloContext> {
	private estimateClient!: EstimateApiClient;

	private context!: ApolloContext;

	initialize(config: DataSourceConfig<ApolloContext>): void {
		this.context = config.context;
		this.estimateClient = new EstimateApiClient({
			rejectAndRetryAfter: RuntimeConfiguration.getInstance().estimateClientRetryTimeout,
			logger: logger.log,
		});
		logger.log.debug(`Initialized estimate data source.`);
	}

	async getOfferMatrix(payload: OfferMatrixInput, { backendPath, smartCowPr }: OfferMatrixOptions) {
		try {
			return this.estimateClient.getOfferMatrix(payload, {
				baseURL: RuntimeConfiguration.getInstance().smartcowEndpoint,
				backendPath,
				prNumber: smartCowPr,
				requestId: this.context.reqId ?? randomUUID(),
			});
		} catch (err) {
			throw new ApolloError(String(err));
		}
	}
}

```

![](https://i.imgur.com/XpWdHFW.png)


### Api Client Generator

#### Command
```bash
yarn nx workspace-generator api-client [name] [description]
```

#### Example

```shell
yarn nx workspace-generator api-client taco "tacos for all"
```
