diff --git a/README.md b/README.md
index 0e6ccdd..e3ae439 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
- Typed wrapper around axios.
+ Typed wrapper around fetch or axios.
@@ -24,7 +24,8 @@
This package's API is still developing and will not follow SEMVER until release 1.0.0.
- HttpClient helps standardarize making HTTP calls and handling when errors are thrown. HttpClient works both in the browser and node environments. Exposes an interface to abort HTTP calls using AbortController. See below about using [AbortController](#using-abortcontroller) in older environments. Exposes an interface to control how requests and responses are handled. See below about using [HttpClient's Request Strategies](#using-request-strategies). Some strategies are provided in this package, but you can also implement your own strategies. List of strategies are provided below.
+HttpClient helps standardizes making HTTP calls regardless of the underlying client used, (fetch is used by default but other clients are available) and handling when errors are thrown. HttpClient works both in the browser and node environments. Exposes an interface to abort HTTP calls using AbortController. See below about using [AbortController](#using-abortcontroller) in older environments. Exposes an interface to control how requests and responses are handled. See below about using [HttpClient's Request Strategies](#using-request-strategies). Some strategies are provided in this package, but you can also implement your own strategies. List of strategies are provided below.
+
Installation
@@ -38,6 +39,7 @@ npm install @seriouslag/httpclient
To see additional examples look in the `src/examples/` directory.
Basic example:
+
```typescript
import { HttpClient } from '@seriouslag/httpclient';
@@ -48,20 +50,20 @@ interface NamedLink {
interface PokemonPage {
count: number;
- next: string|null;
- previous: string|null;
+ next: string | null;
+ previous: string | null;
results: NamedLink[];
}
const httpClient = new HttpClient();
-async function fetchPokemonPage (offset: number = 0, pageSize: number = 20) {
+async function fetchPokemonPage(offset: number = 0, pageSize: number = 20) {
const pokemonApiUrl = 'https://pokeapi.co/api/v2';
return await this.httpClient.get(`${pokemonApiUrl}/pokemon`, {
- params: {
- offset: offset,
- limit: pageSize,
- },
+ params: {
+ offset: offset,
+ limit: pageSize,
+ },
});
}
@@ -72,37 +74,45 @@ async function fetchPokemonPage (offset: number = 0, pageSize: number = 20) {
})();
```
-Configuring axios
+Using axios
+
+We can use axios as the underlying client by installing the `@seriouslag/httpclient-axios` package.
+A custom client adaptor can be provided to the HttpClient constructor, an interface is exposed to allow for custom client adaptors to be created.
+
+```bash
+npm install @seriouslag/httpclient @seriouslag/httpclient-axios
+```
+
Axios can be configured, axios options can be passed into the constructor of HttpClient.
```typescript
import { HttpClient } from '@seriouslag/httpclient';
+import { AxiosClientAdaptor } from '@seriouslag/httpclient-axios';
import { Agent } from 'https';
const httpsAgent = new Agent({
rejectUnauthorized: false,
});
-const httpClient = new HttpClient({
- axiosOptions: {
- httpsAgent,
- },
+const axiosClientAdaptor = new AxiosClientAdaptor({
+ httpsAgent,
});
+
+const httpClient = new HttpClient(axiosClientAdaptor);
```
Using AbortController
Each of the HTTP methods of the HttpClient accept an instance of a AbortController. This allows HTTP requests to be cancelled if not already resolved.
-
```typescript
import { HttpClient } from '@seriouslag/httpclient';
interface PokemonPage {
count: number;
- next: string|null;
- previous: string|null;
+ next: string | null;
+ previous: string | null;
results: NamedLink[];
}
@@ -110,29 +120,34 @@ const pokemonApiUrl = 'https://pokeapi.co/api/v2';
const httpClient = new HttpClient();
const cancelToken = new AbortController();
-const request = httpClient.get(`${pokemonApiUrl}/pokemon`, cancelToken);
+const request = httpClient.get(
+ `${pokemonApiUrl}/pokemon`,
+ cancelToken,
+);
cancelToken.abort();
try {
const result = await request;
- console.log('Expect to not get here because request was aborted.', result)
+ console.log('Expect to not get here because request was aborted.', result);
} catch (e) {
- console.log('Expect to reach here because request was aborted.')
+ console.log('Expect to reach here because request was aborted.');
}
```
+
AbortController in older environments
Abort controller is native to node 15+ and modern browsers. If support is needed for older browsers/node versions then polyfills can be found. This polyfill is used in the Jest test environment for this repository: abortcontroller-polyfill
- ```typescript
- import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only';
- import { HttpClient } from '@seriouslag/httpclient';
+```typescript
+import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only';
+import { HttpClient } from '@seriouslag/httpclient';
+
+const httpClient = new HttpClient();
+```
- const httpClient = new HttpClient();
- ```
Using Request Strategies
@@ -140,6 +155,7 @@ try {
A request strategy is middleware to handle how requests are made and how responses are handled. This is exposed to the consumer using the `HttpRequestStrategy` interface. A request strategy can be passed into the HttpClient (it will be defaulted if not) or it can be passed into each request (if not provided then the strategy provided by the HttpClient will be used). A custom strategy can be provided to the HttpClient's constructor.
Provided strategies:
+
- DefaultHttpRequestStrategy - Throws when a response's status is not 2XX
- ExponentialBackoffRequestStrategy - Retries requests with a backoff. Throws when a response's status is not 2XX
@@ -157,30 +173,31 @@ import { HttpClient, HttpRequestStrategy } from '@seriouslag/httpclient';
class CreatedHttpRequestStrategy implements HttpRequestStrategy {
- /** Passthrough request to axios and check response is created status */
- public async request (client: AxiosInstance, axiosConfig: AxiosRequestConfig) {
- const response = await client.request(axiosConfig);
- this.checkResponseStatus(response);
- return response;
- }
-
- /** Validates the HTTP response is successful created status or throws an error */
- private checkResponseStatus (response: HttpResponse): HttpResponse {
- const isCreatedResponse = response.status === 201;
- if (isCreatedResponse) {
- return response;
- }
- throw response;
- }
+/\*_ Passthrough request to axios and check response is created status _/
+public async request (client: AxiosInstance, axiosConfig: AxiosRequestConfig) {
+const response = await client.request(axiosConfig);
+this.checkResponseStatus(response);
+return response;
+}
+
+/\*_ Validates the HTTP response is successful created status or throws an error _/
+private checkResponseStatus (response: HttpResponse): HttpResponse {
+const isCreatedResponse = response.status === 201;
+if (isCreatedResponse) {
+return response;
+}
+throw response;
+}
}
const httpRequestStrategy = new CreatedHttpRequestStrategy();
// all requests will now throw unless they return an HTTP response with a status of 201
const httpClient = new HttpClient({
- httpRequestStrategy,
+httpRequestStrategy,
});
-```
+
+````
Using Request Strategy in a request
@@ -199,7 +216,7 @@ const httpClient = new HttpClient({
httpRequestStrategy: new DefaultHttpRequestStrategy(),
});
})();
-```
+````
diff --git a/package-lock.json b/package-lock.json
index d8557d0..3babc9b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2609,6 +2609,10 @@
"win32"
]
},
+ "node_modules/@seriouslag/examples": {
+ "resolved": "packages/examples",
+ "link": true
+ },
"node_modules/@seriouslag/httpclient": {
"resolved": "packages/httpclient",
"link": true
@@ -13665,20 +13669,22 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "packages/examples": {
+ "name": "@seriouslag/examples",
+ "version": "0.0.0",
+ "license": "MIT"
+ },
"packages/httpclient": {
"name": "@seriouslag/httpclient",
- "version": "0.0.17",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.16.5"
- }
+ "version": "0.0.19",
+ "license": "MIT"
},
"packages/httpclient-axios": {
"name": "@seriouslag/httpclient-axios",
- "version": "0.0.2",
+ "version": "0.0.4",
"license": "MIT",
"dependencies": {
- "@seriouslag/httpclient": "^0.0.17"
+ "@seriouslag/httpclient": "^0.0.18"
},
"devDependencies": {
"axios": "^1.6.8"
@@ -13686,6 +13692,14 @@
"peerDependencies": {
"axios": "1.x"
}
+ },
+ "packages/httpclient-axios/node_modules/@seriouslag/httpclient": {
+ "version": "0.0.18",
+ "resolved": "https://registry.npmjs.org/@seriouslag/httpclient/-/httpclient-0.0.18.tgz",
+ "integrity": "sha512-iZEWtiOvTBELLMAipG/M9v4uH4jWYjw9grRssEp382J5PQ436rzLcrhU92g45cnB5eaBqu47NIh2DVKdlK2rmg==",
+ "dependencies": {
+ "@babel/runtime": "^7.16.5"
+ }
}
}
}
diff --git a/packages/examples/LICENSE b/packages/examples/LICENSE
new file mode 100644
index 0000000..8181819
--- /dev/null
+++ b/packages/examples/LICENSE
@@ -0,0 +1,7 @@
+Copyright 2024 Landon Gavin
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/packages/examples/package.json b/packages/examples/package.json
new file mode 100644
index 0000000..68b9c61
--- /dev/null
+++ b/packages/examples/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "@seriouslag/examples",
+ "version": "0.0.0",
+ "private": true,
+ "description": "Example code",
+ "scripts": {
+ },
+ "contributors": [
+ {
+ "name": "Landon Gavin",
+ "email": "hi@landongavin.com",
+ "url": "https://landongavin.dev"
+ }
+ ],
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/seriouslag/HttpClient.git"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "keywords": [
+ "httpClient",
+ "typescript",
+ "axios",
+ "fetch"
+ ],
+ "bugs": {
+ "url": "https://github.com/seriouslag/HttpClient/issues"
+ },
+ "author": "hi@landongavin.com",
+ "license": "MIT",
+ "files": [
+ ],
+ "dependencies": {
+ }
+}
diff --git a/packages/examples/src/PokemonApi.ts b/packages/examples/src/PokemonApi.ts
new file mode 100644
index 0000000..bec1829
--- /dev/null
+++ b/packages/examples/src/PokemonApi.ts
@@ -0,0 +1,32 @@
+import { HttpClient } from '@seriouslag/httpclient';
+import { PokemonPage } from './types';
+
+export class PokemonApi {
+ private pageSize = 20;
+
+ constructor(
+ private baseUrl: string,
+ private httpClient: HttpClient,
+ ) {}
+
+ /**
+ * Fetches a page of Pokemon from the API.
+ */
+ public async fetchPokemonPage(
+ cancelToken?: AbortController,
+ offset: number = 0,
+ pageSize: number = this.pageSize,
+ ): Promise {
+ const response = await this.httpClient.get(
+ `${this.baseUrl}/pokemon`,
+ {
+ params: {
+ offset: offset,
+ limit: pageSize,
+ },
+ },
+ cancelToken,
+ );
+ return response;
+ }
+}
diff --git a/packages/examples/src/index.ts b/packages/examples/src/index.ts
new file mode 100644
index 0000000..3ca5019
--- /dev/null
+++ b/packages/examples/src/index.ts
@@ -0,0 +1,2 @@
+export * from './types';
+export * from './PokemonApi';
diff --git a/packages/httpclient/src/examples/types.ts b/packages/examples/src/types.ts
similarity index 100%
rename from packages/httpclient/src/examples/types.ts
rename to packages/examples/src/types.ts
diff --git a/packages/examples/tsconfig.json b/packages/examples/tsconfig.json
new file mode 100644
index 0000000..7d78e2d
--- /dev/null
+++ b/packages/examples/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "baseUrl": ".",
+ "rootDir": "./src",
+ "paths": {
+ "@seriouslag/*": ["../*/src/index"]
+ }
+ },
+ "references": [
+ {
+ "path": "../httpclient"
+ }
+ ],
+ "include": [
+ "./src/**/*.ts"
+ ],
+ "exclude": [
+ "dist/**/*",
+ "node_modules/**/*"
+ ]
+}
diff --git a/packages/httpclient-axios/package.json b/packages/httpclient-axios/package.json
index 983f93a..e4ab4ad 100644
--- a/packages/httpclient-axios/package.json
+++ b/packages/httpclient-axios/package.json
@@ -1,6 +1,6 @@
{
"name": "@seriouslag/httpclient-axios",
- "version": "0.0.3",
+ "version": "0.0.4",
"description": "Typed wrapper HttpClient for axios",
"browser": "dist/index.min.cjs",
"module": "dist/index.esm",
@@ -14,7 +14,8 @@
"type": "module",
"types": "dist/index.d.ts",
"scripts": {
- "build": "rollup -c"
+ "build": "rollup -c",
+ "example:axios": "ts-node src/examples/example-axios-options.ts"
},
"contributors": [
{
diff --git a/packages/httpclient-axios/src/examples/example-axios-options.ts b/packages/httpclient-axios/src/examples/example-axios-options.ts
index c6354e3..6f025a9 100644
--- a/packages/httpclient-axios/src/examples/example-axios-options.ts
+++ b/packages/httpclient-axios/src/examples/example-axios-options.ts
@@ -1,5 +1,5 @@
-import { HttpClient } from '@seriouslag/httpclient/src/HttpClient';
-import { PokemonApi } from '@seriouslag/httpclient/src/examples/PokemonApi';
+import { HttpClient } from '@seriouslag/httpclient';
+import { PokemonApi } from '@seriouslag/examples';
import { AxiosClientAdaptor } from '../index';
import { Agent } from 'https';
diff --git a/packages/httpclient-axios/tsconfig.json b/packages/httpclient-axios/tsconfig.json
index bf432e8..e37c64d 100644
--- a/packages/httpclient-axios/tsconfig.json
+++ b/packages/httpclient-axios/tsconfig.json
@@ -11,6 +11,9 @@
"references": [
{
"path": "../httpclient"
+ },
+ {
+ "path": "../examples"
}
],
"include": [
diff --git a/packages/httpclient/package.json b/packages/httpclient/package.json
index 8a00af2..c9832b3 100644
--- a/packages/httpclient/package.json
+++ b/packages/httpclient/package.json
@@ -1,6 +1,6 @@
{
"name": "@seriouslag/httpclient",
- "version": "0.0.18",
+ "version": "0.0.19",
"description": "Typed wrapper HttpClient for axios",
"browser": "dist/index.min.cjs",
"module": "dist/index.esm",
@@ -53,7 +53,5 @@
"README.md",
"LICENSE"
],
- "dependencies": {
- "@babel/runtime": "^7.16.5"
- }
+ "dependencies": {}
}
diff --git a/packages/httpclient/src/HttpClient.test.ts b/packages/httpclient/src/HttpClient.test.ts
index 78a797b..427fcbd 100644
--- a/packages/httpclient/src/HttpClient.test.ts
+++ b/packages/httpclient/src/HttpClient.test.ts
@@ -246,7 +246,6 @@ describe('HttpClient', () => {
await expect(() => promise).rejects.toThrow();
});
- /** TODO: This is not working yet; Investigate https://github.com/ctimmerm/axios-mock-adapter/issues/59 */
it('fetch - if token is aborted after axios call, AbortError is throw', async () => {
const url = 'www.google.com';
const method = 'get';
diff --git a/packages/httpclient/src/HttpRequestStrategies/HttpRequestStrategy.ts b/packages/httpclient/src/HttpRequestStrategies/HttpRequestStrategy.ts
index 949639e..144b7a1 100644
--- a/packages/httpclient/src/HttpRequestStrategies/HttpRequestStrategy.ts
+++ b/packages/httpclient/src/HttpRequestStrategies/HttpRequestStrategy.ts
@@ -2,6 +2,6 @@ import { HttpResponse, Request } from '../Adaptors';
/** How HTTP calls will be handled. */
export interface HttpRequestStrategy {
- /** Wrapper request around axios to add request and resposne logic */
+ /** Wrapper request around axios to add request and response logic */
request: (request: Request) => Promise>;
}
diff --git a/packages/httpclient/src/HttpRequestStrategies/TimeoutHttpRequestStrategy.test.ts b/packages/httpclient/src/HttpRequestStrategies/TimeoutHttpRequestStrategy.test.ts
index c020605..b334516 100644
--- a/packages/httpclient/src/HttpRequestStrategies/TimeoutHttpRequestStrategy.test.ts
+++ b/packages/httpclient/src/HttpRequestStrategies/TimeoutHttpRequestStrategy.test.ts
@@ -87,7 +87,7 @@ describe('TimeoutHttpRequestStrategy', () => {
try {
await strategy.request(request);
- fail('it will not reach here');
+ throw new Error('it will not reach here');
} catch (e) {
const error = e as Partial>;
expect(error.statusText).toEqual(failedResponseData.statusText);
diff --git a/packages/httpclient/src/HttpRequestStrategies/TimeoutHttpRequestStrategy.ts b/packages/httpclient/src/HttpRequestStrategies/TimeoutHttpRequestStrategy.ts
index 10cc559..ad62e7e 100644
--- a/packages/httpclient/src/HttpRequestStrategies/TimeoutHttpRequestStrategy.ts
+++ b/packages/httpclient/src/HttpRequestStrategies/TimeoutHttpRequestStrategy.ts
@@ -1,5 +1,6 @@
import { DefaultHttpRequestStrategy } from './DefaultHttpRequestStrategy';
import { Request, HttpResponse } from '../Adaptors';
+import { TimeoutError } from '../errors/TimeoutError';
/** This strategy is used to set a timeout on a request */
export class TimeoutHttpRequestStrategy extends DefaultHttpRequestStrategy {
@@ -10,12 +11,12 @@ export class TimeoutHttpRequestStrategy extends DefaultHttpRequestStrategy {
super();
}
- public override async request(
+ public override request(
request: Request,
): Promise> {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
- reject(new Error('Request timed out'));
+ reject(new TimeoutError('Request timed out'));
}, this.timeout);
super
.request(request)
diff --git a/packages/httpclient/src/HttpRequestStrategies/index.ts b/packages/httpclient/src/HttpRequestStrategies/index.ts
index fc9552d..5fa6d9e 100644
--- a/packages/httpclient/src/HttpRequestStrategies/index.ts
+++ b/packages/httpclient/src/HttpRequestStrategies/index.ts
@@ -2,3 +2,4 @@ export * from './HttpRequestStrategy';
export * from './DefaultHttpRequestStrategy';
export * from './MaxRetryHttpRequestStrategy';
export * from './ExponentialBackoffRequestStrategy';
+export * from './TimeoutHttpRequestStrategy';
diff --git a/packages/httpclient/src/errors/TimeoutError.ts b/packages/httpclient/src/errors/TimeoutError.ts
new file mode 100644
index 0000000..0b4a368
--- /dev/null
+++ b/packages/httpclient/src/errors/TimeoutError.ts
@@ -0,0 +1,7 @@
+import { HttpError } from './HttpError';
+
+export class TimeoutError extends HttpError {
+ constructor(message: string, options?: ErrorOptions) {
+ super(message, options);
+ }
+}
diff --git a/packages/httpclient/src/examples/PokemonApi.ts b/packages/httpclient/src/examples/PokemonApi.ts
deleted file mode 100644
index 5f35b54..0000000
--- a/packages/httpclient/src/examples/PokemonApi.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { HttpClient } from '../HttpClient';
-import { PokemonPage } from './types';
-
-export class PokemonApi {
- private pageSize = 20;
-
- constructor (private baseUrl: string, private httpClient: HttpClient) {}
-
- /** */
- public async fetchPokemonPage (cancelToken?: AbortController, offset: number = 0, pageSize: number = this.pageSize): Promise {
- const response = await this.httpClient.get(`${this.baseUrl}/pokemon`, {
- params: {
- offset: offset,
- limit: pageSize,
- },
- }, cancelToken);
- return response;
- }
-}
-
diff --git a/packages/httpclient/src/examples/example-basic.ts b/packages/httpclient/src/examples/example-basic.ts
index a2a5f5d..18265b0 100644
--- a/packages/httpclient/src/examples/example-basic.ts
+++ b/packages/httpclient/src/examples/example-basic.ts
@@ -1,5 +1,5 @@
import { HttpClient } from '../HttpClient';
-import { PokemonApi } from './PokemonApi';
+import { PokemonApi } from '@seriouslag/examples';
const pokemonApiUrl = 'https://pokeapi.co/api/v2';
diff --git a/packages/httpclient/src/examples/example-cancelToken.ts b/packages/httpclient/src/examples/example-cancelToken.ts
index 6eba3aa..c24b5dc 100644
--- a/packages/httpclient/src/examples/example-cancelToken.ts
+++ b/packages/httpclient/src/examples/example-cancelToken.ts
@@ -1,5 +1,5 @@
import { HttpClient } from '../HttpClient';
-import { PokemonApi } from './PokemonApi';
+import { PokemonApi } from '@seriouslag/examples';
const pokemonApiUrl = 'https://pokeapi.co/api/v2';
diff --git a/packages/httpclient/tsconfig.json b/packages/httpclient/tsconfig.json
index e3abe37..f8ddc90 100644
--- a/packages/httpclient/tsconfig.json
+++ b/packages/httpclient/tsconfig.json
@@ -7,9 +7,13 @@
"@seriouslag/*": ["../*/src/index"]
}
},
- "references": [],
+ "references": [
+ {
+ "path": "../examples"
+ }
+ ],
"include": [
- "./src/**/*.ts"
+ "./src/**/*.ts",
],
"exclude": [
"dist/**/*",