1.0.0 • Published 3 years ago

api-actions v1.0.0

Weekly downloads
-
License
MIT
Repository
github
Last release
3 years ago

About

This package provides a simple interface to dispatch async FSA-compliant actions based on API responses.

Installation

npm i -S api-actions

In the file where you initialize your redux store, import the api-actions middleware, pass it your axios instance, and include it as middleware.

import APIActions from 'api-actions';
import {configureStore} from '@reduxjs/toolkit';
import {combineReducers} from 'redux';
import axios from 'axios'

const axiosInstance = axios.create({...});

export default configureStore({
  reducer: combineReducers({...}),
  middleware: (gdm) => gdm().concat(APIActions(axiosInstance)),
})

Usage

The api-actions middleware takes an axios instance upon initalization and intercepts a RSAA (Redux Standard API-Calling Action). These are actions identified by the presence of an [RSAA] key, where RSAA is a String constant defined by the api-actions middleware. To create a RSAA action compatible with the api-actions middleware, you must use the createAPIAction method, which takes an object describing your API request as a parameter. Here is an example:

import { createAPIAction } from 'api-actions';
...
dispatch(createAPIAction({
  path: 'http://example.com/create_action',
  method: 'GET',
  types: ['REQUEST', 'SUCCESS', 'FAIL']
})

The createAPIAction parameter object is typed as the following:

{
  path: string;
  method: HTTPMethod;
  body?: PlainObject | ((getState: () => any) => PlainObject);
  types: TypeArray;
  onReqSuccess?: (getState: () => any, res: AxiosResponse, axios: AxiosInstance) => void;
  onReqFail?: (getState: () => any, err: AxiosError, axios: AxiosInstance) => void;
  config?: AxiosRequestConfig;
}

Let's dissect this.

  • Path: This field is self-explanatory - this is where you pass the URL for your request. Note: this field may also represent just an endpoint if you have set a baseURL for the axios instance passed to api-actions.
  • Method: One of the HTTP methods.
  • Body: This may be a JSON object or a function that takes the current state of your redux store as an argument and returns a JSON object.
  • Types: The TypeArray is defined as the following:
[string, string | RequestDescriptor, string]

It is an array of three elements, where the first string is the type of the action to be dispatched before the request is made - this may be used to handle loading. The second element describes the action to be dispatched if the API returns a response with a status code in the 2xx range. The last element specifies the type of the action dispatched on failure (AxiosError, NetworkError or InternalError - more about error handling in a bit). If the second element is just a string (TYPE of action dispatched on success), the dispatched action will default to the following:

{ 
  type: [TYPE],
  payload: res.data // res is the AxiosResponse recieved
}

However, there may be situations in which you may want to customize the payload of the above action - let's say you want to dispatch res.statusCode instead. This can be done by passing a RequestDescriptor instead of a string. A RequestDescriptor is an object of the following shape:

{
  type: string;
  payload: (getState: () => any, res: AxiosResponse) => PlainObject;
}

Hence, to dispatch res.statusCode as the payload, you would use the following array to define types:

['REQUEST_TYPE', 
  { 
    type: 'SUCCESS_TYPE', 
    payload: (_, res) => res.statusCode 
   }, 
 'FAIL_TYPE']

This types definition would dispatch the following before the request is made:

{ type: 'REQUEST_TYPE' }

On recieving a successful response, the dispatched action will be of the following form:

{ 
  type: 'SUCCESS_TYPE', 
  payload: '2xx' // res.statusCode
}

Finally, the action dispatched on error will be the following (more details about error handling here):

{
  type: 'FAIL_TYPE', 
  payload: [ERROR OBJECT], 
  error: true 
}
  • onReqSuccess: This is a function that runs to completion upon recieving a successful response and before dispatching the success action. It takes the current state of the redux store, the response object and the axios instance as arguments. One example usage of this function could be to set new default headers to the axios instance based on the response. This would be acheived by the following function:
...
onReqSuccess: (_, res, axios) => axios.defaults.headers.common['Authorization'] = `BEARER ${res.data.new_access_token}`
...
  • onReqFail: Similar to the previous function, this runs to completion upon recieving an AxiosError (either an API error or network error) and before dispatching the error action.
  • config: One benefit of passing a axios instance to api-actions is that this allows to set default AxiosRequestConfig options to ba applied to all your requests. However, some requests may need to override these defaults. To do this, you could pass in a custom config object.

Error Handling

In accordance with FSA, in the case of an error, an action of the following form is returned:

{ 
  type: [FAIL_TYPE],
  payload: [ERROR OBJECT],
  error: true
}

There are three types of error classes that api-actions may return - InternalError, NetworkError, RequestError. InternalError is thrown when a user-defined function fails (such as the function passed in the request descriptor), NetworkError is thrown when the API is unreachable, and RequestError is thrown when the API returns a response with statusCode in the 4xx/5xx range. Each of these error classes extend the CustomError class, which defined as the following:

class CustomError extends Error {
  __error: any;
  constructor(name: string, message: string, err: any) {
    super(message);
    this.name = name;
    this.__error = err;
  }
}

The __error property wraps the original error thrown. For example, when an API returns a response with statusCode 400 (i.e Bad Request), the following action is dispatched:

{
  type: [FAIL_TYPE],
  payload: {
    name: 'RequestError',
    message: 'Request failed with error code 400',
    __error: [AxiosError object]
  },
  error: true
}

Testing

This package is unit tested using the mocha and chai libraries. Utilizing axios-mock-adapter and redux-mock-store, most test code follows this format:

// This snippet tests whether a FAIL_ACTION_TYPE is dispatched on receiving a 404 error from the API
it('should dispatch FAIL_ACTION_TYPE on RequestError', async () => {
    // define mock endpoint
    mockAxiosClient.onGet('/test').reply(404);
  
    // array of expected actions to be dispatched by api-actions
    const expectActions = [
      {
        type: 'REQUEST_ACTION_TYPE',
      },
      {
        type: 'FAIL_ACTION_TYPE',
        payload: new RequestError('RequestError: Request failed with status code 404', {}),
        error: true,
      },
    ];
    
    // dispatch APIAction to mock store
    await store.dispatch({
      ...createAPIAction({
        path: '/test',
        types: ['REQUEST_ACTION_TYPE', 'SUCCESS_ACTION_TYPE', 'FAIL_ACTION_TYPE'],
      }),
      type: 'TESTING',
    });

    expect(store.getActions()).shallowDeepEqual(expectActions);
 });

License

MIT

Acknowledgements

This package was inspired by the following sources: