Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/hardcodet/httpclient-js
Simple HTTP/API client for typescript / javascript projects.
https://github.com/hardcodet/httpclient-js
javascript react react-native rest-client retries typescript
Last synced: 1 day ago
JSON representation
Simple HTTP/API client for typescript / javascript projects.
- Host: GitHub
- URL: https://github.com/hardcodet/httpclient-js
- Owner: hardcodet
- License: mit
- Created: 2020-04-23T23:15:07.000Z (over 4 years ago)
- Default Branch: master
- Last Pushed: 2023-10-09T09:51:29.000Z (about 1 year ago)
- Last Synced: 2024-03-23T00:06:51.095Z (9 months ago)
- Topics: javascript, react, react-native, rest-client, retries, typescript
- Language: TypeScript
- Size: 49.8 KB
- Stars: 1
- Watchers: 2
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.MD
- License: LICENSE
Awesome Lists containing this project
README
# Typescript / Javascript API Client
This is an opinionated HTTP client that provides simple access to REST-based APIs. It sits on
top of `axios` (https://github.com/axios/axios) and provides result unwrapping
(with generics support for Typescript), pluggable authentication strategies
and basic retry logic.
Also supports on-the-fly class transformations (JSON to real classes) and validation
based on `class-transformer` and `class-validator`.```
public async getUser(): Promise {
const httpClient = new HttpClient("https://www.foo.com/api");
const result: ApiResult = await httpClient.getAs("users/123");
return result.getValueOrThrow();
}
```## Installation
Using NPM:
```
npm i @hardcodet/httpclient
```Using Yarn:
```
yarn add @hardcodet/httpclient
```#### Optional: Class transformations / validation setup
If you are using the built-in class transformations based on `class-transformer`,
you will also need `reflect-metadata`:```
npm i reflect-metadata
-- or --
yarn add reflect-metadata
```Then import it in a global place (typically your app initialization):
```
import "reflect-metadata";
```## Basic Usage
Syntax is quite straightforward:
1. Issue an HTTP call
2. Process the `ApiResponse` (for `get`, `post`, ...) or `ApiResult`
(for `getAs`, `postAs`, ...) and handle the result. The `ApiResult` class provides
a `value` property that can be used to get and unwrap the parsed JSON response.```
public async doWork(): Promise {const httpClient = new HttpClient("https://www.foo.com/api");
const uri = "/v1/bar");
const payload = { ... };
// send a POST with the specified body
const response: ApiResult = await httpClient.postAs(uri, payload);
// unwrap the returned data (throws exception if the request fails)
const result: SomeDto = response.getValueOrThrow();
return result;
}
```
Alternatively, you can inspect the response object, e.g.```
if (!response.success) {
if(response.notFound) {
// we got a 404
return undefined;
} else {
// some other error - throw
throw new Error(resonse.createError());
}
} else {
const result: SomeDto = response.value;
return result;
}
```
If you don't expect a result, you can use `ensureSuccess`, which will
throw an error in case you won't get an `HTTP 2xx`:```
const response: ApiResponse = await httpClient.post("some/endpoint");
response.ensureSuccess();
```## Authentication
`HttpClient` provides strategy-based authentication through the `IAuthClient` interface that
can be simply injected into a `HttpClient` instance:```
const basicAuth = new BasicAuthClient("myUserName", "myPassword");
const httpClient = new HttpClient("https://www.foo.com/api", {authClient: basicAuth});
const result = await httpClient.get("protected/endpoint");
```Every time an invoked endpoint returns an `HTTP 401`, the client will try to resolve
a token through an injected auth strategy (if one is available).There's currently 3 built-in implementations:
- `Basic` auth (user name / password)
- OAuth client credentials grant
- A delegate-based strategy that allows you to inject some custom token fetch logic.
The resolved token will then be submitted as a `Bearer` token with subsequent requests.### Delegation based auth
Here's a short sample with delegation. We simply use a second `HttpClient` without
authentication to fetch the token of the main `httpClient`:```
constructor() {
const authClient = new DelegateBearerAuthClient(() => this.getAccessToken());
this.httpClient = new HttpClient("https://api.foo.com", { authClient });
}/**
* Invoked by the delegation authentication strategy of the HTTP client in order to
* get a new access token when needed.
*/
private async getAccessToken(): Promise {
// use an independent HTTP client - the default one would block because it's
// waiting on this method to resolve a token
const tokenClient = new HttpClient("https://www.auth-provider.com");const uri = "v1/login?userId=foo&&password=bar";
const result = await tokenClient.postAs(uri);
return result.getValueOrThrow();
}
```### Custom authentication strategies
`IAuthClient` basically just provides a contract to perform a token fetch/refresh, and to
construct an authorization header value that is being added to the request header when submitting
a request. You can easily build your own.```
export interface IAuthClient {/**
* Asynchronously refreshes the token.
*/
refreshToken(): Promise;/**
* Updates the header to be sent with an HTTP
* request in order to provide authentication.
*/
getAuthHeader(): Promise;
}
```## Retries
The package comes with a simple retry mechanism. By default, it will perform up to 2 retries
(3 attempts in total) before giving up in case the invoked endpoint returns a `5xx` error.For `3xx` (redirects) or `4xx` errors, it will fail immediately without retries.)
### Retry delays
Delays between retries can follow 3 possible patterns:
- Constant delays, e.g. 2 seconds between retries)
- Linearly increasing delays, e.g. 2, 4, 6, 8 seconds between retries)
- Exponentially increasing delays (default), e.g. 1, 4, 9, 16, 25 seconds between retries)```
// up to 4 retries with 5 seconds wait time each
const options: HttpClientOptions = {
maxAttempts: 5,
retryDelay: 5000,
retryStrategy: RetryStrategy.Constant,
};
const httpClient = new HttpClient("https://www.foo.com/api", options);
```## Transformation and Validation
(Note: if you just need global transformations of date strings to `Date`, read below
on Json Processors and `IsoDateProcessor` specifically.)Consider this DTO:
```
class User {
firstName: string;
lastName: string;
email: string;
dateOfBirth: Date;getFullName(): string {
return firstName + " " + lastName;
}
}
```Note that if you fetch the JSON that matches this DTO from an API, the
returned object is *not* an instance of `User` but a plain Javascript object.
Accordingly, you don't have a `getFullName` method, and `dateOfBirth` is actually
a string, not a `Date`. The snippet below would fail:```
// get user
const result: ApiResult = await httpClient.getAs("users/123");
const user: User = result.value;// will fail - there is no such method on the returned object!
const fullName: string = user.getFullName();
```In order to get around this, you can use the transformation feature of the
library. Note the additional `User` type parameter in the `getAs` method:```
// get user
const result: ApiResult = await httpClient.getAs("users/123", User);
const user: User = result.value;// works!
const fullName: string = user.getFullName();
```#### Type conversions
There is still one gotcha: The javascript runtime still has no idea that the
`dateOfBirth` field should be a Date, since JSON declares dates as regular
strings. In order to transform that string into a `Date` instance,
you will have to decorate your `UserDto.dateOfBirth` field with the
`@Type(() => Date)` decorator:```
@Type(() => Date)
dateOfBirth: Date;
```You will also need the `Type` decorator for nested types.
All transformation comes from the `class-transformer` package. For more information,
see https://github.com/typestack/class-transformer.#### Validation
Transformed types can also be validated based on validation decorators from
the `class-validator` package. For example, in order to make sure the returned
user data contains a valid email address, decorate it like this:```
@IsEmail()
email: string;
```For more information on validation, see https://github.com/typestack/class-validator.
#### Simpler version: JSON Processors
A simpler alternative to decorating every DTO you have is injecting a global JSON
processor. If we review our `User` DTO above, the `getFullName` method may be
an anti-pattern anyway: The DTO should only capture state. This leaves us with
a pretty prototypical use case: We just want all date strings to be parsed into
actual `Date` objects.```
interface User {
firstName: string;
lastName: string;
email: string;
dateOfBirth: Date; // NEEDS TO BE TRANSFORMED
}
```An alternative here is to use a JSON processor which transforms incoming or
outgoing JSON. And because date transformations are so common, there's the
built-in `IsoDateProcessor` that we can use right away:```
const c = new HttpClient(options);
c.inboundProcessors.push(new IsoDateProcessor())
```The injected `IsoDateProcessor` will process retrieved JSON objects
and transform any string that matches an ISO8601 date for us.```
// get user
const result: ApiResult = await httpClient.getAs("users/123");
const user: User = result.value;// works, since dateOfBirth is a Date object now
const year: string = user.dateOfBirth.getFullYear();
```For more flexibility, just check the `IJsonProcessor` interface and the built-in
`StringTransformJsonProcessor` class.## Options
```
const defaultOptions: HttpClientOptions = {
timeout: 10000,
maxAttempts: 3,
retryDelay: 1000,
retryStrategy: RetryStrategy.Exponential,
authClient: undefined,
customHeaders: undefined,
};
```|Option Value |Description |Default |
|-------------|------------------------------------------------------------------------|-------------------------|
|timeout |Max time for a request until it fails. |10000 (ms) |
|maxAttempts |Maximum attempts until the client gives up. Set to 1 to disable retries.|3 |
|retryDelay |Base delay between retries. Actual delay depends on the retry strategy. |1000 (ms) |
|retryStrategy|Constantly, linearly or exponentially growing delays. |RetryStrategy.Exponential|
|authClient |Pluggable authentication strategy. |- |
|customHeaders|Custom headers to be submitted with every request. |- |