Socket
Socket
Sign inDemoInstall

@hardcodet/httpclient

Package Overview
Dependencies
14
Maintainers
1
Versions
22
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

    @hardcodet/httpclient

HTTP client library for easy consumption of RESTful APIs. Comes with built-in authentication strategies, validation and retry mechanism.


Version published
Weekly downloads
20
increased by5.26%
Maintainers
1
Created
Weekly downloads
 

Readme

Source

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<User> {
    const httpClient = new HttpClient("https://www.foo.com/api");
    const result: ApiResult<User> = await httpClient.getAs<User>("users/123");
    return result.getValueOrThrow();
}

Installation

NPM Version

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<SomeDto> {

    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<SomeDto> = await httpClient.postAs<SomeDto>(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<string> {
    // 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<string>(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<void>;

    /**
     * Updates the header to be sent with an HTTP
     * request in order to provide authentication.
     */
    getAuthHeader(): Promise<object>;
}

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<User> = await httpClient.getAs<User>("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<User> = 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<User> = await httpClient.getAs<User>("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 ValueDescriptionDefault
timeoutMax time for a request until it fails.10000 (ms)
maxAttemptsMaximum attempts until the client gives up. Set to 1 to disable retries.3
retryDelayBase delay between retries. Actual delay depends on the retry strategy.1000 (ms)
retryStrategyConstantly, linearly or exponentially growing delays.RetryStrategy.Exponential
authClientPluggable authentication strategy.-
customHeadersCustom headers to be submitted with every request.-

FAQs

Last updated on 22 Oct 2023

Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc