npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@hardcodet/httpclient

v3.0.0

Published

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

Downloads

30

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