4
\$\begingroup\$

Description

I've developed an application which use axios to communicate with the PayPal API. PayPal has a NodeJS SDK, but unfortunately this doesn't support the Subscription API, so I'm using axios to handle the API requests.

The code is working well, but I want improve the Access Token logic. Essentially the PayPal token expiry is 1 hour, and I don't want to get a new token each time I perform a request.

So I've used a mechanism that allows me to store the token in my own database and through the interceptors of Axios I retrieve that token. If the token is expired, I get a new one.

Code

First of all, I've declared the axios instance, I don't want use the global instance 'cause I could reuse axios for other API payment gateways such as Stripe:

const qs = require('qs');
const axios = require('axios');
const tokenUtils = require('./token-utils');

const baseURL = 'https://api.sandbox.paypal.com';

let instance = axios.create({
    baseURL
})

then I defined the interceptors which is something like a middleware and is executed before any request:

instance.interceptors.response.use(

    // Return response as no errors raised
    function (response) {
        return response
    },

    function (error) {
        const errorResponse = error.response;

        // Check if token is expired
        if (isTokenExpiredError(errorResponse)) {
            return resetTokenAndReattemptRequest(error)
        }

        // Error was not generated by the expired token
        return Promise.reject(error);
    }
)

then I have the isTokenExpiredError which simply checks if the token is expired looking at the http status code:

function isTokenExpiredError(errorResponse) {
    return (errorResponse.status == 401) ? true : false;
}

Next, I have the resetTokenAndReattemptRequest function which actually checks if the token is expired and eventually gets a new one:

let isAlreadyFetchingAccessToken = false;

async function resetTokenAndReattemptRequest(error) {

    try {
        const { response } = error;

        // Get the stored token
        const accessToken = await tokenUtils.getResetToken();

        // Start the token generation task
        // Add the failed request to the subscriber
        const retryOriginalRequest = new Promise(resolve => {
            addSubscriber(newToken => {
                response.config.headers.Authorization = 'Bearer ' + newToken;
                resolve(axios(response.config));
            });
        });

        // There are no fetchint token request and the access token is expired (no length)
        if (!isAlreadyFetchingAccessToken && accessToken === "") {
            isAlreadyFetchingAccessToken = true;

            const response = await axios({
                method: 'POST',
                url: `${baseURL}/v1/oauth2/token`,
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                    'Access-Control-Allow-Credentials': true
                },
                data: qs.stringify({
                    grant_type: 'client_credentials'
                }),
                auth: {
                    username: "your paypal client id",
                    password: "your paypal secret id"
                }
            });

            // An error happened when asking for a new token
            if (!response.data) {
                return Promise.reject(error);
            }

            const newToken = response.data.access_token;

            // Store generated token in db for a next usage
            tokenUtils.saveToken(newToken);

            isAlreadyFetchingAccessToken = false;
            onAccessTokenFetched(newToken);
        }

        return retryOriginalRequest;
    }
    catch (err) {
        return Promise.reject(err);
    }
}

I commented the whole code, hoping to be much clear as possible, feel free to ask any question eventually. Essentially, a failed request is added to the subscribers which is handled by the following:

let subscribers = [];

function addSubscriber(callback) {
    subscribers.push(callback);
}

each subscribed request is execute after the token generation by:

function onAccessTokenFetched(accessToken) {
    subscribers.forEach(callback => callback(accessToken));
    subscribers = [];
}

So an API request will look like this:

instance.get(
    `${baseURL}/v1/billing/subscriptions/I-0SUB6KG4203B`)
    .then(response => {
        console.log(response.data);
        return response.data;
    }).catch((error) => {
        console.log('error');
        return null;
    });

I also have a tokenUtils which contains two function:

  • getAccessToken: get the token stored from a database
  • storeAccessToken: store a token to the db

I've also uploaded the full code here, you may take a look.

\$\endgroup\$
0

1 Answer 1

1
+50
\$\begingroup\$

General Thoughts

I haven't used the paypal API before but did obtain credentials and ran the code. I don't see much that would improve the logic to fetch the token but do see small improvements that can be made.

Review

A common convention is to have constants in ALL_CAPS - so baseURL can be converted to BASE_URL E.g. per Google JS style guide, airbnb style guide. While it isn't totally immutable unless frozen it signifies to readers that it should not be modified.

For more information on the topic, see the MDN documentation about constants, as well as answers to this SO question.

A more appropriate name than isTokenExpiredError would be isResponseUnauthenticated since the 401 error means "Authentication failed due to invalid authentication credentials."1

return (errorResponse.status == 401) ? true : false;

This is overly verbose - it could be simplified to:

return (errorResponse.status == 401)

There is only one usage of errorResponse after it is assigned - it can be eliminated by substituting error.response in the one place it is used.

The variable subscribers could be truncated with .length = 0 in onAccessTokenFetched() and then be declared with const because it wouldn't be re-assigned. This helps avoid accidental re-assignment in the future when the code is expanded.

The request made with axios() to get the token could be simplified using axios.post(url[, data[, config]])

const response = await axios.post(`${baseURL}/v1/oauth2/token`, data, {...})
\$\endgroup\$
3
  • \$\begingroup\$ Thanks for your answer! I just have some questions: 1. Should be all the constants ALL_CAPS? Even those inside the methods? 2. Could you suggest a specific folder name convention where I can add this Axios interceptors? \$\endgroup\$
    – sfarzoso
    May 9, 2020 at 8:50
  • 1
    \$\begingroup\$ 1. I would only use that for hard-coded values, which likely won’t change ever, and not other variables even if they shouldn’t be reassigned - e.g. an array like subscribers. 2. You could make a wrapper that returns/exports axios Or instance- maybe call it axios-instance, axios-wrapper or something similar \$\endgroup\$ May 9, 2020 at 23:53
  • \$\begingroup\$ Thansk for your suggests I've appreciated \$\endgroup\$
    – sfarzoso
    May 10, 2020 at 8:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.