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 => {
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 === "") {

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.

## 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.

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, {...})

• 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? May 9, 2020 at 8:50
• 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 May 9, 2020 at 23:53
• Thansk for your suggests I've appreciated May 10, 2020 at 8:38