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

@swappable/httpclient

v2.0.2

Published

Facilitate the swappability of http client dependencies in a Node.js project.

Downloads

64

Readme

httpClient

Facilitate the swappability of HTTP client dependencies in a Node.js project.

Swappability

Dependency injection's fraternal twin.

Dependencies, and the security vulnerabilities that come with them, are burdens on node.js project maintainers. We want to promote code that makes replacing 1 dependency with another, easy.

Some call this the façade design pattern.

Case in point:

The request npm package used to be a dependency for millions of node projects.

Then came this update:

As of Feb 11th 2020, request is fully deprecated. No new changes are expected [to] land. In fact, none have landed for some time.

Axios and node-fetch presented themselves as obvious choices to migrate to. At some point the fetch API is probably going to become native to node. Even then, what is to say that something better won't surface down the road?

This project reduces the time and efforts required to transition from request to axios to fetch API to whatever is the new shiny thing today.

One amazing side-effect of using this library, is that it also organically promotes good unit tests, because now you can easily mock responses for any http request.

How to use

Example with Axios

// buildHttpClient.js
const getClientBuilder = require('@swappable/httpclient');
const axios = require('axios').default;

const buildHttpClient = getClientBuilder({
  requestAdapter: (req) => {
      console.debug(req);
      return axios(req);
  },
  responseAdapter: (res) => res.data,
  errorAdapter: (e) => {
      if (e.response) {
          const { status, statusText, data } = e.response;
          const error = new Error(`${status} ${statusText}`);
          error.data = data;
          throw error;
      }
      const errorMsg = e.isAxiosError ? 'Could not reach the API' : e.message;
      throw new Error(errorMsg || 'Something went awry and you can probably fix it');
  },
});

module.exports = buildHttpClient;

getClientBuilder returns a function (named buildHttpClient here) that allows you to spin up as many http Clients as your project needs.

You might want that because your node.js app might make requests to 2+ different APIs, and no 2 APIs are the same.

For example, imagine an app that fetches data from your company, and then posts a message to a Slack channel. There's at least 1 call to your company's API and 1 call to the Slack API.

One of the interesting things about the Slack API, is that it rarely returns any status other than 200. What you get instead is an { ok: false, ...etc } payload.

Maybe you'd like to write logic so that not ok responses throw errors instead, and you'd very much like to write this logic only once. So:

// in another file eg: slackApiClient.js
// make use of the buildHttpClient you just created
const buildHttpClient = require('./path/to/buildHttpClient');

const slackApiClient = buildHttpClient({
  baseUrl: 'https://slack.com/api',
  successHandler: (res) => {
    if (res.ok) {
      return res;
    }
    throw {
      response: {
        status: 'ok false',
        data: res,
      }
    };
  },
  setDefaultHeaders: () => ({
    'Content-Type': 'application/json; charset=utf-8',
  }),
});

slackApiClient.postMessage = (channel, message) => {
  return slackApiClient.post('chat.postMessage', {
    channel,
    text: message,
  });
};

Adjacent to this in your app, your company's API is secured by OAuth, and you'd like the logic for automatically refreshing tokens to be writen only once as well. Here is how this might look like:

// myCompanyApiClient.js
const buildHttpClient = require('./path/to/buildHttpClient');

function MyCompanyApiClient() {
  this.token = 'originalTokenBoundToExpire';
  this.apiClient = buildHttpClient({
    baseUrl: 'https://mycompany.com/api',
    failureHandler: (error, originalRequet) => {
      if (error.status === 403 && originalRequet.attemptNumber < 2) {
        return this.refreshOAuthToken()
          .then(() => this.apiClient.send({
            ...originalRequet,
            attemptNumber: originalRequet.attemptNumber + 1,
            headers: {
              ...originalRequet.headers,
              'Authorization': `Bearer ${this.token}`,
            },
          }));
      }
      if (error.payload) {
        const humanReadableMessage = error.payload.reason;
        error.message = `My company Api responded with [${error.status}] ${humanReadableMessage}`;
      }
      throw error;
    },
    setDefaultHeaders: () => ({
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${this.token}`,
    }),
    setFixedHeaders: () => ({
      'User-Agent': 'My app (5.0.5)'
    }),
  });
}

MyCompanyApiClient.prototype.refreshOAuthToken = function refreshOAuthToken() {
  this.token = 'refreshedTokenOverwritingOriginalToken';
  return Promise.resolve(this.token);
};

MyCompanyApiClient.prototype.fetchUsers = function fetchUsers() {
  return this.apiClient.get('/users');
};

requestAdapter

Adapts the request to the swappable dependency. Http client libraries like axios or fetch always expose a function that actually sends the request over the wire. Their signatures vary, hence the adapter.

It will always be called with:

{
  "headers": { "or": null },
  "method": "UPPERCASE http method",
  "url": "fully qualified url",
  "attemptNumber": 1
  "data": "json string or undefined"
}

The adapter must return a promise.

responseAdapter & errorAdapter

These are the callbacks that will always be called when a promise settles, for all clients.

In other words, this is about transforming the API responses and errors from the swappable dependency, regardless of which API returns or throws them.

You can think of it as a vendor adapter, where the vendor is the swappable library (eg: axios).

This is a good place to add debug logs.

If really you're happy with the response or error just the way the vendor delivers them, then just do:

const responseAdapter = res => res;
const errorAdapter = err => { throw err };

successHandler & failureHandler

These are the optional callbacks that you may want called when a promise settles, for a specific client.

In other words, this is about transforming the API responses and errors of a specific API, regardless of which library executes the request.

This is a good place to handle OAuth token expirations.

Both customSuccessAdapter & customFailureAdapter receive the original request as their 2nd argument.

const successHandler = (res, req) => /* do your thing */;
const failureHandler = (err, req) => /* do your thing */;

setDefaultHeaders < setFixedHeaders < per-request headers < null

The latter will take precedence over the former.

What this means is: If the consumer specifies headers on a per-request basis, like so: apiClient.get(url, { headers }), while also specifying the setFixedHeaders and setDefaultHeaders functions:

  1. setDefaultHeaders does not even get called for this request
  2. the return value of setFixedHeaders is merged with the request headers, but when 2 keys match, the per-request specification is used
  3. every single key of the per-request headers is guaranteed to be sent to the api
  4. every key of the fixed headers that isn't overridden by the per-request headers, is sent
  5. apiClient.get(url, { headers: null }) specifies that no headers should be sent with this request

setDefaultHeaders

If provided, it is called automatically when no headers are provided on a per-request basis.

// setDefaultHeaders will be called
apiClient
  .get('https://lol.com')
  .catch(console.error);

// setDefaultHeaders will not be called
const headers = { 'Content-Type': 'application/json' };
apiClient
  .get('https://lol.com', { headers })
  .catch(console.error);

setFixedHeaders

If provided, it is called automatically unless headers are explicitly set to null. This is useful if, for example, you want to specify an additional "User-Agent" header on all requests, regardless of how the other headers are set.

// apiClient was instantiated with
// setFixedHeaders: () => ({ 'Content-Type': 'application/json' })

apiClient
  .get('https://lol.com')
  .catch(console.error);
// applied as headers === { 'Content-Type': 'application/json' }

const headers = { 'timeout': 5000 };
apiClient
  .get('https://lol.com', { headers })
  .catch(console.error);
// merged as headers === { 'Content-Type': 'application/json', 'timeout': 5000 }

const headers = { 'Content-Type': 'x-www-form-urlencoded' };
apiClient
  .post('https://lol.com/auth', 'grant_type=password+stuff=etc', { headers })
  .catch(console.error);
// overridden as headers === { 'Content-Type': 'x-www-form-urlencoded' }

apiClient
  .get('https://lol.com', { headers: null })
  .catch(console.error);
// setFixedHeaders was not called

Methods of the produced clients

  • get: (url, { queryParams, pathParams, headers } = {}) => Promise
  • post: (url, data, { pathParams, headers } = {}) => Promise
  • put: (url, data, { pathParams, headers } = {}) => Promise
  • patch: (url, data, { pathParams, headers } = {}) => Promise
  • delete: (url, { pathParams, headers } = {}) => Promise
  • options: (url, { pathParams, headers } = {}) => Promise And for when you need full control over the request:
  • send: ({ headers, method, url, data, queryParams, pathParams, attemptNumber }) => Promise
    • pathParams is an array of strings used to build a /path/appended/to/url
    • queryParams is an object literal used to build a ?query=string&appendedto=url%26path
    • attemptNumber is the number of time this request has been attempted. Defaults to 1

When assessing whether this tool is right for you, take a look at the sandbox and test files, to see it being used.